diff --git a/src/main/java/com/tiedup/remake/rig/anim/AnimationClip.java b/src/main/java/com/tiedup/remake/rig/anim/AnimationClip.java new file mode 100644 index 0000000..54a6ec9 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/AnimationClip.java @@ -0,0 +1,140 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.mutable.MutableInt; + +import net.minecraft.util.Mth; + +public class AnimationClip { + public static final AnimationClip EMPTY_CLIP = new AnimationClip(); + + protected Map jointTransforms = new HashMap<> (); + protected float clipTime; + protected float[] bakedTimes; + + /// To modify existing keyframes in runtime and keep the baked state, call [#setBaked] again + /// after finishing clip modification. (Frequent calls of this method will cause a performance issue) + public void addJointTransform(String jointName, TransformSheet sheet) { + this.jointTransforms.put(jointName, sheet); + this.bakedTimes = null; + } + + public boolean hasJointTransform(String jointName) { + return this.jointTransforms.containsKey(jointName); + } + + /// Bakes all keyframes to optimize calculating current pose, + public void bakeKeyframes() { + Set timestamps = new HashSet<> (); + + this.jointTransforms.values().forEach(transformSheet -> { + transformSheet.forEach((i, keyframe) -> { + timestamps.add(keyframe.time()); + }); + }); + + float[] bakedTimestamps = new float[timestamps.size()]; + MutableInt mi = new MutableInt(0); + + timestamps.stream().sorted().toList().forEach(f -> { + bakedTimestamps[mi.getAndAdd(1)] = f; + }); + + Map bakedJointTransforms = new HashMap<> (); + + this.jointTransforms.forEach((jointName, transformSheet) -> { + bakedJointTransforms.put(jointName, transformSheet.createInterpolated(bakedTimestamps)); + }); + + this.jointTransforms = bakedJointTransforms; + this.bakedTimes = bakedTimestamps; + } + + /// Bake keyframes supposing all keyframes are aligned (mainly used when creating link animations) + public void setBaked() { + TransformSheet transformSheet = this.jointTransforms.get("Root"); + + if (transformSheet != null) { + this.bakedTimes = new float[transformSheet.getKeyframes().length]; + + for (int i = 0; i < transformSheet.getKeyframes().length; i++) { + this.bakedTimes[i] = transformSheet.getKeyframes()[i].time(); + } + } + } + + public TransformSheet getJointTransform(String jointName) { + return this.jointTransforms.get(jointName); + } + + public final Pose getPoseInTime(float time) { + Pose pose = new Pose(); + + if (time < 0.0F) { + time = this.clipTime + time; + } + + if (this.bakedTimes != null && this.bakedTimes.length > 0) { + // Binary search + int begin = 0, end = this.bakedTimes.length - 1; + + while (end - begin > 1) { + int i = begin + (end - begin) / 2; + + if (this.bakedTimes[i] <= time && this.bakedTimes[i+1] > time) { + begin = i; + end = i+1; + break; + } else { + if (this.bakedTimes[i] > time) { + end = i; + } else if (this.bakedTimes[i+1] <= time) { + begin = i; + } + } + } + + float delta = Mth.clamp((time - this.bakedTimes[begin]) / (this.bakedTimes[end] - this.bakedTimes[begin]), 0.0F, 1.0F); + TransformSheet.InterpolationInfo iInfo = new TransformSheet.InterpolationInfo(begin, end, delta); + + for (String jointName : this.jointTransforms.keySet()) { + pose.putJointData(jointName, this.jointTransforms.get(jointName).getInterpolatedTransform(iInfo)); + } + } else { + for (String jointName : this.jointTransforms.keySet()) { + pose.putJointData(jointName, this.jointTransforms.get(jointName).getInterpolatedTransform(time)); + } + } + + return pose; + } + + /// @return returns protected keyframes of each joint to keep the baked state of keyframes. + public Map getJointTransforms() { + return Collections.unmodifiableMap(this.jointTransforms); + } + + public void reset() { + this.jointTransforms.clear(); + this.bakedTimes = null; + } + + public void setClipTime(float clipTime) { + this.clipTime = clipTime; + } + + public float getClipTime() { + return this.clipTime; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/AnimationManager.java b/src/main/java/com/tiedup/remake/rig/anim/AnimationManager.java new file mode 100644 index 0000000..0151aa7 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/AnimationManager.java @@ -0,0 +1,514 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.logging.log4j.Logger; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.client.Minecraft; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.FileToIdConverter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.GsonHelper; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.fml.event.IModBusEvent; +import com.tiedup.remake.rig.anim.property.AnimationProperty; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.asset.JsonAssetLoader; +import com.tiedup.remake.rig.anim.client.AnimationSubFileReader; +import yesman.epicfight.api.data.reloader.SkillManager; +import com.tiedup.remake.rig.exception.AssetLoadingException; +import com.tiedup.remake.rig.util.InstantiateInvoker; +import com.tiedup.remake.rig.util.MutableBoolean; +import yesman.epicfight.gameasset.Animations; +import yesman.epicfight.gameasset.Armatures; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.main.EpicFightSharedConstants; +import yesman.epicfight.network.EpicFightNetworkManager; +import yesman.epicfight.network.client.CPCheckAnimationRegistryMatches; +import yesman.epicfight.network.server.SPDatapackSync; + +@SuppressWarnings("unchecked") +public class AnimationManager extends SimplePreparableReloadListener> { + private static final AnimationManager INSTANCE = new AnimationManager(); + private static ResourceManager serverResourceManager = null; + private static final Gson GSON = new GsonBuilder().create(); + private static final String DIRECTORY = "animmodels/animations"; + + public static AnimationManager getInstance() { + return INSTANCE; + } + + private final Map> animationById = Maps.newHashMap(); + private final Map> animationByName = Maps.newHashMap(); + private final Map, StaticAnimation> animations = Maps.newHashMap(); + private final Map, String> resourcepackAnimationCommands = Maps.newHashMap(); + + public static boolean checkNull(AssetAccessor animation) { + if (animation == null || animation.isEmpty()) { + if (animation != null) { + EpicFightMod.stacktraceIfDevSide("Empty animation accessor: " + animation.registryName(), NoSuchElementException::new); + } else { + EpicFightMod.stacktraceIfDevSide("Null animation accessor", NoSuchElementException::new); + } + + return true; + } + + return false; + } + + public static AnimationAccessor byKey(String registryName) { + return byKey(ResourceLocation.parse(registryName)); + } + + public static AnimationAccessor byKey(ResourceLocation registryName) { + return (AnimationAccessor)getInstance().animationByName.get(registryName); + } + + public static AnimationAccessor byId(int animationId) { + return (AnimationAccessor)getInstance().animationById.get(animationId); + } + + public Map> getAnimations(Predicate> filter) { + Map> filteredItems = + this.animationByName.entrySet().stream() + .filter(entry -> { + return filter.test(entry.getValue()); + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + return ImmutableMap.copyOf(filteredItems); + } + + public AnimationClip loadAnimationClip(StaticAnimation animation, BiFunction clipLoader) { + try { + if (getAnimationResourceManager() == null) { + return null; + } + + JsonAssetLoader modelLoader = new JsonAssetLoader(getAnimationResourceManager(), animation.getLocation()); + AnimationClip loadedClip = clipLoader.apply(modelLoader, animation); + + return loadedClip; + } catch (AssetLoadingException e) { + throw new AssetLoadingException("Failed to load animation clip from: " + animation, e); + } + } + + public static void readAnimationProperties(StaticAnimation animation) { + ResourceLocation dataLocation = getSubAnimationFileLocation(animation.getLocation(), AnimationSubFileReader.SUBFILE_CLIENT_PROPERTY); + ResourceLocation povLocation = getSubAnimationFileLocation(animation.getLocation(), AnimationSubFileReader.SUBFILE_POV_ANIMATION); + + getAnimationResourceManager().getResource(dataLocation).ifPresent((rs) -> { + AnimationSubFileReader.readAndApply(animation, rs, AnimationSubFileReader.SUBFILE_CLIENT_PROPERTY); + }); + + getAnimationResourceManager().getResource(povLocation).ifPresent((rs) -> { + AnimationSubFileReader.readAndApply(animation, rs, AnimationSubFileReader.SUBFILE_POV_ANIMATION); + }); + } + + @Override + protected List prepare(ResourceManager resourceManager, ProfilerFiller profilerIn) { + if (!EpicFightSharedConstants.isPhysicalClient() && serverResourceManager == null) { + serverResourceManager = resourceManager; + } + + this.animations.clear(); + this.animationById.entrySet().removeIf(entry -> !entry.getValue().inRegistry()); + this.animationByName.entrySet().removeIf(entry -> !entry.getValue().inRegistry()); + this.resourcepackAnimationCommands.clear(); + + List directories = new ArrayList<> (); + scanDirectoryNames(resourceManager, directories); + + return directories; + } + + private static void scanDirectoryNames(ResourceManager resourceManager, List output) { + FileToIdConverter filetoidconverter = FileToIdConverter.json(DIRECTORY); + filetoidconverter.listMatchingResources(resourceManager).keySet().stream().map(AnimationManager::pathToId).forEach(output::add); + } + + @Override + protected void apply(List objects, ResourceManager resourceManager, ProfilerFiller profilerIn) { + Armatures.reload(resourceManager); + + Set registeredAnimation = + this.animationById.values().stream() + .reduce( + new HashSet<> (), + (set, accessor) -> { + set.add(accessor.registryName()); + + for (AssetAccessor subAnimAccessor : accessor.get().getSubAnimations()) { + set.add(subAnimAccessor.registryName()); + } + + return set; + }, + (set1, set2) -> { + set1.addAll(set2); + return set1; + } + ); + + // Load animations that are not registered by AnimationRegistryEvent + // Reads from /assets folder in physical client, /datapack in physical server. + objects.stream() + .filter(animId -> !registeredAnimation.contains(animId) && !animId.getPath().contains("/data/") && !animId.getPath().contains("/pov/")) + .sorted(Comparator.comparing(ResourceLocation::toString)) + .forEach(animId -> { + Optional resource = resourceManager.getResource(idToPath(animId)); + + try (Reader reader = resource.orElseThrow().openAsReader()) { + JsonElement jsonelement = GsonHelper.fromJson(GSON, reader, JsonElement.class); + this.readResourcepackAnimation(animId, jsonelement.getAsJsonObject()); + } catch (IOException | JsonParseException | IllegalArgumentException resourceReadException) { + EpicFightMod.LOGGER.error("Couldn't parse animation data from {}", animId, resourceReadException); + } catch (Exception e) { + EpicFightMod.LOGGER.error("Failed at constructing {}", animId, e); + } + }); + + SkillManager.reloadAllSkillsAnimations(); + + this.animations.entrySet().stream() + .reduce( + new ArrayList>(), + (list, entry) -> { + MutableBoolean init = new MutableBoolean(true); + + if (entry.getValue() == null || entry.getValue().getAccessor() == null) { + EpicFightMod.logAndStacktraceIfDevSide(Logger::error, "Invalid animation implementation: " + entry.getKey(), AssetLoadingException::new); + init.set(false); + } + + entry.getValue().getSubAnimations().forEach((subAnimation) -> { + if (subAnimation == null || subAnimation.get() == null) { + EpicFightMod.logAndStacktraceIfDevSide(Logger::error, "Invalid sub animation implementation: " + entry.getKey(), AssetLoadingException::new); + init.set(false); + } + }); + + if (init.value()) { + list.add(entry.getValue().getAccessor()); + list.addAll(entry.getValue().getSubAnimations()); + } + + return list; + }, + (list1, list2) -> { + list1.addAll(list2); + return list1; + } + ) + .forEach(accessor -> { + accessor.doOrThrow(StaticAnimation::postInit); + + if (EpicFightSharedConstants.isPhysicalClient()) { + AnimationManager.readAnimationProperties(accessor.get()); + } + }); + } + + public static ResourceLocation getSubAnimationFileLocation(ResourceLocation location, AnimationSubFileReader.SubFileType subFileType) { + int splitIdx = location.getPath().lastIndexOf('/'); + + if (splitIdx < 0) { + splitIdx = 0; + } + + return ResourceLocation.fromNamespaceAndPath(location.getNamespace(), String.format("%s/" + subFileType.getDirectory() + "%s", location.getPath().substring(0, splitIdx), location.getPath().substring(splitIdx))); + } + + /// Converts animation id, acquired by [StaticAnimation#getRegistryName], to animation resource path acquired by [StaticAnimation#getLocation] + public static ResourceLocation idToPath(ResourceLocation rl) { + return rl.getPath().matches(DIRECTORY + "/.*\\.json") ? rl : ResourceLocation.fromNamespaceAndPath(rl.getNamespace(), DIRECTORY + "/" + rl.getPath() + ".json"); + } + + /// Converts animation resource path, acquired by [StaticAnimation#getLocation], to animation id acquired by [StaticAnimation#getRegistryName] + public static ResourceLocation pathToId(ResourceLocation rl) { + return ResourceLocation.fromNamespaceAndPath(rl.getNamespace(), rl.getPath().replace(DIRECTORY + "/", "").replace(".json", "")); + } + + public static void setServerResourceManager(ResourceManager pResourceManager) { + serverResourceManager = pResourceManager; + } + + public static ResourceManager getAnimationResourceManager() { + return EpicFightSharedConstants.isPhysicalClient() ? Minecraft.getInstance().getResourceManager() : serverResourceManager; + } + + public int getResourcepackAnimationCount() { + return this.resourcepackAnimationCommands.size(); + } + + public Stream getResourcepackAnimationStream() { + return this.resourcepackAnimationCommands.entrySet().stream().map((entry) -> { + CompoundTag compTag = new CompoundTag(); + compTag.putString("registry_name", entry.getKey().registryName().toString()); + compTag.putInt("id", entry.getKey().id()); + compTag.putString("invoke_command", entry.getValue()); + + return compTag; + }); + } + + /** + * @param mandatoryPack : creates dummy animations for animations from the server without animation clips when the server has mandatory resource pack. + * custom weapon types & mob capabilities won't be created because they won't be able to find the animations from the server + * dummy animations will be automatically removed right after reloading resourced as the server forces using resource pack + */ + @OnlyIn(Dist.CLIENT) + public void processServerPacket(SPDatapackSync packet, boolean mandatoryPack) { + if (mandatoryPack) { + for (CompoundTag tag : packet.getTags()) { + String invocationCommand = tag.getString("invoke_command"); + ResourceLocation registryName = ResourceLocation.parse(tag.getString("registry_name")); + int id = tag.getInt("id"); + + if (this.animationByName.containsKey(registryName)) { + continue; + } + + AnimationAccessor accessor = AnimationAccessorImpl.create(registryName, getResourcepackAnimationCount(), false, (accessor$2) -> { + try { + return InstantiateInvoker.invoke(invocationCommand, StaticAnimation.class).getResult(); + } catch (Exception e) { + EpicFightMod.LOGGER.warn("Failed at creating animation from server resource pack"); + e.printStackTrace(); + return Animations.EMPTY_ANIMATION; + } + }); + + this.animationById.put(id, accessor); + this.animationByName.put(registryName, accessor); + } + } + + int animationCount = this.animations.size(); + String[] registryNames = new String[animationCount]; + + for (int i = 0; i < animationCount; i++) { + String registryName = this.animationById.get(i + 1).registryName().toString(); + registryNames[i] = registryName; + } + + CPCheckAnimationRegistryMatches registrySyncPacket = new CPCheckAnimationRegistryMatches(animationCount, registryNames); + EpicFightNetworkManager.sendToServer(registrySyncPacket); + } + + public void validateClientAnimationRegistry(CPCheckAnimationRegistryMatches msg, ServerGamePacketListenerImpl connection) { + StringBuilder messageBuilder = new StringBuilder(); + int count = 0; + + Set clientAnimationRegistry = new HashSet<> (Set.of(msg.registryNames)); + + for (String registryName : this.animations.keySet().stream().map((rl) -> rl.toString()).toList()) { + if (!clientAnimationRegistry.contains(registryName)) { + // Animations that don't exist in client + if (count < 10) { + messageBuilder.append(registryName); + messageBuilder.append("\n"); + } + + count++; + } else { + clientAnimationRegistry.remove(registryName); + } + } + + // Animations that don't exist in server + for (String registryName : clientAnimationRegistry) { + if (registryName.equals("empty")) { + continue; + } + + if (count < 10) { + messageBuilder.append(registryName); + messageBuilder.append("\n"); + } + + count++; + } + + if (count >= 10) { + messageBuilder.append(Component.translatable("gui.epicfight.warn.animation_unsync.etc", (count - 9)).getString()); + messageBuilder.append("\n"); + } + + if (!messageBuilder.isEmpty()) { + connection.disconnect(Component.translatable("gui.epicfight.warn.animation_unsync", messageBuilder.toString())); + } + } + + private static final Set NO_WARNING_MODID = Sets.newHashSet(); + + public static void addNoWarningModId(String modid) { + NO_WARNING_MODID.add(modid); + } + + /************************************************** + * User-animation loader + **************************************************/ + @SuppressWarnings({ "deprecation" }) + private void readResourcepackAnimation(ResourceLocation rl, JsonObject json) throws Exception { + JsonElement constructorElement = json.get("constructor"); + + if (constructorElement == null) { + if (NO_WARNING_MODID.contains(rl.getNamespace())) { + return; + } else { + EpicFightMod.logAndStacktraceIfDevSide( + Logger::error + , "Datapack animation reading failed: No constructor information has provided: " + rl + , IllegalStateException::new + , "No constructor information has provided in User animation, " + rl + "\nPlease remove this resource if it's unnecessary to optimize your project." + ); + return; + } + } + + JsonObject constructorObject = constructorElement.getAsJsonObject(); + String invocationCommand = constructorObject.get("invocation_command").getAsString(); + StaticAnimation animation = InstantiateInvoker.invoke(invocationCommand, StaticAnimation.class).getResult(); + JsonElement propertiesElement = json.getAsJsonObject().get("properties"); + + if (propertiesElement != null) { + JsonObject propertiesObject = propertiesElement.getAsJsonObject(); + + for (Map.Entry entry : propertiesObject.entrySet()) { + AnimationProperty propertyKey = AnimationProperty.getSerializableProperty(entry.getKey()); + Object value = propertyKey.parseFrom(entry.getValue()); + animation.addPropertyUnsafe(propertyKey, value); + } + } + + AnimationAccessor accessor = AnimationAccessorImpl.create(rl, this.animations.size() + 1, false, null); + animation.setAccessor(accessor); + + this.resourcepackAnimationCommands.put(accessor, invocationCommand); + this.animationById.put(accessor.id(), accessor); + this.animationByName.put(accessor.registryName(), accessor); + this.animations.put(accessor, animation); + } + + + + public interface AnimationAccessor extends AssetAccessor { + int id(); + + default boolean idBetween(AnimationAccessor a1, AnimationAccessor a2) { + return a1.id() <= this.id() && a2.id() >= this.id(); + } + } + + public static record AnimationAccessorImpl (ResourceLocation registryName, int id, boolean inRegistry, Function, A> onLoad) implements AnimationAccessor { + private static AnimationAccessor create(ResourceLocation registryName, int id, boolean inRegistry, Function, A> onLoad) { + return new AnimationAccessorImpl (registryName, id, inRegistry, onLoad); + } + + @Override + public A get() { + if (!INSTANCE.animations.containsKey(this)) { + INSTANCE.animations.put(this, this.onLoad.apply(this)); + } + + return (A)INSTANCE.animations.get(this); + } + + public String toString() { + return this.registryName.toString(); + } + + public int hashCode() { + return this.registryName.hashCode(); + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof AnimationAccessor armatureAccessor) { + return this.registryName.equals(armatureAccessor.registryName()); + } else if (obj instanceof ResourceLocation rl) { + return this.registryName.equals(rl); + } else if (obj instanceof String name) { + return this.registryName.toString().equals(name); + } else { + return false; + } + } + } + + public static class AnimationRegistryEvent extends Event implements IModBusEvent { + private List builders = Lists.newArrayList(); + private Set namespaces = Sets.newHashSet(); + + public void newBuilder(String namespace, Consumer build) { + if (this.namespaces.contains(namespace)) { + throw new IllegalArgumentException("Animation builder namespace '" + namespace + "' already exists!"); + } + + this.namespaces.add(namespace); + this.builders.add(new AnimationBuilder(namespace, build)); + } + + public List getBuilders() { + return this.builders; + } + } + + public static record AnimationBuilder(String namespace, Consumer task) { + public AnimationManager.AnimationAccessor nextAccessor(String id, Function, T> onLoad) { + AnimationAccessor accessor = AnimationAccessorImpl.create(ResourceLocation.fromNamespaceAndPath(this.namespace, id), INSTANCE.animations.size() + 1, true, onLoad); + + INSTANCE.animationById.put(accessor.id(), accessor); + INSTANCE.animationByName.put(accessor.registryName(), accessor); + INSTANCE.animations.put(accessor, null); + + return accessor; + } + } +} diff --git a/src/main/java/com/tiedup/remake/rig/anim/AnimationPlayer.java b/src/main/java/com/tiedup/remake/rig/anim/AnimationPlayer.java new file mode 100644 index 0000000..154b298 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/AnimationPlayer.java @@ -0,0 +1,162 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import com.mojang.datafixers.util.Pair; + +import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier; +import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackTimeModifier; +import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import yesman.epicfight.gameasset.Animations; +import yesman.epicfight.main.EpicFightSharedConstants; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class AnimationPlayer { + protected float elapsedTime; + protected float prevElapsedTime; + protected boolean isEnd; + protected boolean doNotResetTime; + protected boolean reversed; + protected AssetAccessor play; + + public AnimationPlayer() { + this.setPlayAnimation(Animations.EMPTY_ANIMATION); + } + + public void tick(LivingEntityPatch entitypatch) { + DynamicAnimation currentPlay = this.getAnimation().get(); + DynamicAnimation currentPlayStatic = currentPlay.getRealAnimation().get(); + this.prevElapsedTime = this.elapsedTime; + + float playbackSpeed = currentPlay.getPlaySpeed(entitypatch, currentPlay); + PlaybackSpeedModifier playSpeedModifier = currentPlayStatic.getProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER).orElse(null); + + if (playSpeedModifier != null) { + playbackSpeed = playSpeedModifier.modify(currentPlay, entitypatch, playbackSpeed, this.prevElapsedTime, this.elapsedTime); + } + + this.elapsedTime += EpicFightSharedConstants.A_TICK * playbackSpeed * (this.isReversed() && currentPlay.canBePlayedReverse() ? -1.0F : 1.0F); + PlaybackTimeModifier playTimeModifier = currentPlayStatic.getProperty(StaticAnimationProperty.ELAPSED_TIME_MODIFIER).orElse(null); + + if (playTimeModifier != null) { + Pair time = playTimeModifier.modify(currentPlay, entitypatch, playbackSpeed, this.prevElapsedTime, this.elapsedTime); + this.prevElapsedTime = time.getFirst(); + this.elapsedTime = time.getSecond(); + } + + if (this.elapsedTime > currentPlay.getTotalTime()) { + if (currentPlay.isRepeat()) { + this.prevElapsedTime = this.prevElapsedTime - currentPlay.getTotalTime(); + this.elapsedTime %= currentPlay.getTotalTime(); + } else { + this.elapsedTime = currentPlay.getTotalTime(); + currentPlay.end(entitypatch, null, true); + this.isEnd = true; + } + } else if (this.elapsedTime < 0) { + if (currentPlay.isRepeat()) { + this.prevElapsedTime = currentPlay.getTotalTime() - this.elapsedTime; + this.elapsedTime = currentPlay.getTotalTime() + this.elapsedTime; + } else { + this.elapsedTime = 0.0F; + currentPlay.end(entitypatch, null, true); + this.isEnd = true; + } + } + } + + public void reset() { + this.elapsedTime = 0; + this.prevElapsedTime = 0; + this.isEnd = false; + } + + public void setPlayAnimation(AssetAccessor animation) { + if (this.doNotResetTime) { + this.doNotResetTime = false; + this.isEnd = false; + } else { + this.reset(); + } + + this.play = animation; + } + + public Pose getCurrentPose(LivingEntityPatch entitypatch, float partialTicks) { + return this.play.get().getPoseByTime(entitypatch, this.prevElapsedTime + (this.elapsedTime - this.prevElapsedTime) * partialTicks, partialTicks); + } + + public float getElapsedTime() { + return this.elapsedTime; + } + + public float getPrevElapsedTime() { + return this.prevElapsedTime; + } + + public void setElapsedTimeCurrent(float elapsedTime) { + this.elapsedTime = elapsedTime; + this.isEnd = false; + } + + public void setElapsedTime(float elapsedTime) { + this.elapsedTime = elapsedTime; + this.prevElapsedTime = elapsedTime; + this.isEnd = false; + } + + public void setElapsedTime(float prevElapsedTime, float elapsedTime) { + this.elapsedTime = elapsedTime; + this.prevElapsedTime = prevElapsedTime; + this.isEnd = false; + } + + public void begin(AssetAccessor animation, LivingEntityPatch entitypatch) { + animation.get().tick(entitypatch); + } + + public AssetAccessor getAnimation() { + return this.play; + } + + public AssetAccessor getRealAnimation() { + return this.play.get().getRealAnimation(); + } + + public void markDoNotResetTime() { + this.doNotResetTime = true; + } + + public boolean isEnd() { + return this.isEnd; + } + + public void terminate(LivingEntityPatch entitypatch) { + this.play.get().end(entitypatch, this.play, true); + this.isEnd = true; + } + + public boolean isReversed() { + return this.reversed; + } + + public void setReversed(boolean reversed) { + this.reversed = reversed; + } + + public boolean isEmpty() { + return this.play == Animations.EMPTY_ANIMATION; + } + + @Override + public String toString() { + return this.getAnimation() + " " + this.prevElapsedTime + " " + this.elapsedTime; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/AnimationVariables.java b/src/main/java/com/tiedup/remake/rig/anim/AnimationVariables.java new file mode 100644 index 0000000..259a244 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/AnimationVariables.java @@ -0,0 +1,267 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import net.minecraft.resources.ResourceLocation; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.util.ParseUtil; +import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap; +import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap.TypeKey; +import yesman.epicfight.gameasset.Animations; +import yesman.epicfight.network.common.AnimationVariablePacket; + +public class AnimationVariables { + protected final Animator animator; + protected final TypeFlexibleHashMap> animationVariables = new TypeFlexibleHashMap<> (false); + + public AnimationVariables(Animator animator) { + this.animator = animator; + } + + public Optional getSharedVariable(SharedAnimationVariableKey key) { + return Optional.ofNullable(this.animationVariables.get(key)); + } + + @SuppressWarnings("unchecked") + public T getOrDefaultSharedVariable(SharedAnimationVariableKey key) { + return ParseUtil.orElse((T)this.animationVariables.get(key), () -> key.defaultValue(this.animator)); + } + + @SuppressWarnings("unchecked") + public Optional get(IndependentAnimationVariableKey key, AssetAccessor animation) { + if (animation == null) { + return Optional.empty(); + } + + Map subMap = this.animationVariables.get(key); + + if (subMap == null) { + return Optional.empty(); + } else { + return Optional.ofNullable((T)subMap.get(animation.registryName())); + } + } + + @SuppressWarnings("unchecked") + public T getOrDefault(IndependentAnimationVariableKey key, AssetAccessor animation) { + if (animation == null) { + return Objects.requireNonNull(key.defaultValue(this.animator), "Null value returned by default provider."); + } + + Map subMap = this.animationVariables.get(key); + + if (subMap == null) { + return Objects.requireNonNull(key.defaultValue(this.animator), "Null value returned by default provider."); + } else { + return ParseUtil.orElse((T)subMap.get(animation.registryName()), () -> key.defaultValue(this.animator)); + } + } + + public void putDefaultSharedVariable(SharedAnimationVariableKey key) { + T value = key.defaultValue(this.animator); + Objects.requireNonNull(value, "Null value returned by default provider."); + + this.putSharedVariable(key, value); + } + + public void putSharedVariable(SharedAnimationVariableKey key, T value) { + this.putSharedVariable(key, value, true); + } + + @SuppressWarnings("unchecked") + @Deprecated // Avoid direct use + public void putSharedVariable(SharedAnimationVariableKey key, T value, boolean synchronize) { + if (this.animationVariables.containsKey(key) && !key.mutable()) { + throw new UnsupportedOperationException("Can't modify a const variable"); + } + + this.animationVariables.put((AnimationVariableKey)key, value); + + if (synchronize && key instanceof SynchedAnimationVariableKey) { + SynchedAnimationVariableKey synchedanimationvariablekey = (SynchedAnimationVariableKey)key; + synchedanimationvariablekey.sync(this.animator.entitypatch, (AssetAccessor)null, value, AnimationVariablePacket.Action.PUT); + } + } + + public void putDefaultValue(IndependentAnimationVariableKey key, AssetAccessor animation) { + T value = key.defaultValue(this.animator); + Objects.requireNonNull(value, "Null value returned by default provider."); + + this.put(key, animation, value); + } + + public void put(IndependentAnimationVariableKey key, AssetAccessor animation, T value) { + this.put(key, animation, value, true); + } + + @SuppressWarnings("unchecked") + @Deprecated // Avoid direct use + public void put(IndependentAnimationVariableKey key, AssetAccessor animation, T value, boolean synchronize) { + if (animation == Animations.EMPTY_ANIMATION) { + return; + } + + this.animationVariables.computeIfPresent(key, (k, v) -> { + Map variablesByAnimations = ((Map)v); + + if (!key.mutable() && variablesByAnimations.containsKey(animation.registryName())) { + throw new UnsupportedOperationException("Can't modify a const variable"); + } + + variablesByAnimations.put(animation.registryName(), value); + + return v; + }); + + this.animationVariables.computeIfAbsent(key, (k) -> { + return new HashMap<> (Map.of(animation.registryName(), value)); + }); + + if (synchronize && key instanceof SynchedAnimationVariableKey) { + SynchedAnimationVariableKey synchedanimationvariablekey = (SynchedAnimationVariableKey)key; + synchedanimationvariablekey.sync(this.animator.entitypatch, animation, value, AnimationVariablePacket.Action.PUT); + } + } + + public T removeSharedVariable(SharedAnimationVariableKey key) { + return this.removeSharedVariable(key, true); + } + + @SuppressWarnings("unchecked") + @Deprecated // Avoid direct use + public T removeSharedVariable(SharedAnimationVariableKey key, boolean synchronize) { + if (!key.mutable()) { + throw new UnsupportedOperationException("Can't remove a const variable"); + } + + if (synchronize && key instanceof SynchedAnimationVariableKey) { + SynchedAnimationVariableKey synchedanimationvariablekey = (SynchedAnimationVariableKey)key; + synchedanimationvariablekey.sync(this.animator.entitypatch, null, null, AnimationVariablePacket.Action.REMOVE); + } + + return (T)this.animationVariables.remove(key); + } + + @SuppressWarnings("unchecked") + public void removeAll(AnimationAccessor animation) { + if (animation == Animations.EMPTY_ANIMATION) { + return; + } + + for (Map.Entry, Object> entry : this.animationVariables.entrySet()) { + if (entry.getKey().isSharedKey()) { + continue; + } + + Map map = (Map)entry.getValue(); + + if (map != null) { + map.remove(animation.registryName()); + } + } + } + + public void remove(IndependentAnimationVariableKey key, AssetAccessor animation) { + this.remove(key, animation, true); + } + + @SuppressWarnings("unchecked") + @Deprecated // Avoid direct use + public void remove(IndependentAnimationVariableKey key, AssetAccessor animation, boolean synchronize) { + if (animation == Animations.EMPTY_ANIMATION) { + return; + } + + Map map = (Map)this.animationVariables.get(key); + + if (map != null) { + map.remove(animation.registryName()); + } + + if (synchronize && key instanceof SynchedAnimationVariableKey) { + SynchedAnimationVariableKey synchedanimationvariablekey = (SynchedAnimationVariableKey)key; + synchedanimationvariablekey.sync(this.animator.entitypatch, null, null, AnimationVariablePacket.Action.REMOVE); + } + } + + public static SharedAnimationVariableKey shared(Function defaultValueSupplier, boolean mutable) { + return new SharedAnimationVariableKey<> (defaultValueSupplier, mutable); + } + + public static IndependentAnimationVariableKey independent(Function defaultValueSupplier, boolean mutable) { + return new IndependentAnimationVariableKey<> (defaultValueSupplier, mutable); + } + + protected abstract static class AnimationVariableKey implements TypeKey { + protected final Function defaultValueSupplier; + protected final boolean mutable; + + protected AnimationVariableKey(Function defaultValueSupplier, boolean mutable) { + this.defaultValueSupplier = defaultValueSupplier; + this.mutable = mutable; + } + + @NonNull + public T defaultValue(Animator animator) { + return this.defaultValueSupplier.apply(animator); + } + + public boolean mutable() { + return this.mutable; + } + + @Override + public T defaultValue() { + throw new UnsupportedOperationException("Use defaultValue(Animator animator) to get default value of animation variable key"); + } + + public abstract boolean isSharedKey(); + public abstract boolean isSynched(); + } + + public static class SharedAnimationVariableKey extends AnimationVariableKey { + protected SharedAnimationVariableKey(Function initValueSupplier, boolean mutable) { + super(initValueSupplier, mutable); + } + + @Override + public boolean isSharedKey() { + return true; + } + + @Override + public boolean isSynched() { + return false; + } + } + + public static class IndependentAnimationVariableKey extends AnimationVariableKey { + protected IndependentAnimationVariableKey(Function initValueSupplier, boolean mutable) { + super(initValueSupplier, mutable); + } + + @Override + public boolean isSharedKey() { + return false; + } + + @Override + public boolean isSynched() { + return false; + } + } +} diff --git a/src/main/java/com/tiedup/remake/rig/anim/Animator.java b/src/main/java/com/tiedup/remake/rig/anim/Animator.java new file mode 100644 index 0000000..b34abe7 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/Animator.java @@ -0,0 +1,145 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.Map; +import java.util.Optional; + +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.mojang.datafixers.util.Pair; + +import net.minecraftforge.common.MinecraftForge; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.types.EntityState; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.event.InitAnimatorEvent; +import yesman.epicfight.gameasset.Animations; +import yesman.epicfight.main.EpicFightMod; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public abstract class Animator { + protected final Map> livingAnimations = Maps.newHashMap(); + protected final AnimationVariables animationVariables = new AnimationVariables(this); + protected final LivingEntityPatch entitypatch; + + public Animator(LivingEntityPatch entitypatch) { + this.entitypatch = entitypatch; + } + + /** + * Play an animation + * + * @param nextAnimation the animation that is meant to be played. + * @param transitionTimeModifier extends the transition time if positive value provided, or starts in time as an amount of time (e.g. -0.1F starts in 0.1F frame time) + */ + public abstract void playAnimation(AssetAccessor nextAnimation, float transitionTimeModifier); + + public final void playAnimation(int id, float transitionTimeModifier) { + this.playAnimation(AnimationManager.byId(id), transitionTimeModifier); + } + + /** + * Play a given animation without transition animation. + * @param nextAnimation + */ + public abstract void playAnimationInstantly(AssetAccessor nextAnimation); + + public final void playAnimationInstantly(int id) { + this.playAnimationInstantly(AnimationManager.byId(id)); + } + + /** + * Reserve a given animation until the current animation ends. + * If the given animation has a higher priority than current animation, it terminates the current animation by force and play the next animation + * @param nextAnimation + */ + public abstract void reserveAnimation(AssetAccessor nextAnimation); + + public final void reserveAnimation(int id) { + this.reserveAnimation(AnimationManager.byId(id)); + } + + /** + * Stop playing given animation if exist + * @param targetAnimation + * @return true when found and successfully stop the target animation + */ + public abstract boolean stopPlaying(AssetAccessor targetAnimation); + + /** + * Play an shooting animation to end aiming pose + */ + public abstract void playShootingAnimation(); + + public final boolean stopPlaying(int id) { + return this.stopPlaying(AnimationManager.byId(id)); + } + + public abstract void setSoftPause(boolean paused); + public abstract void setHardPause(boolean paused); + public abstract void tick(); + + public abstract EntityState getEntityState(); + + /** + * Searches an animation player playing the given animation parameter or return base layer if it's null + * Secure non-null but returned animation player won't match with a given animation + */ + @Nullable + public abstract AnimationPlayer getPlayerFor(@Nullable AssetAccessor playingAnimation); + + /** + * Searches an animation player playing the given animation parameter + */ + @Nullable + public abstract Optional getPlayer(AssetAccessor playingAnimation); + + public abstract Pair findFor(Class animationType); + public abstract Pose getPose(float partialTicks); + + public void postInit() { + InitAnimatorEvent initAnimatorEvent = new InitAnimatorEvent(this.entitypatch, this); + MinecraftForge.EVENT_BUS.post(initAnimatorEvent); + } + + public void playDeathAnimation() { + this.playAnimation(this.livingAnimations.getOrDefault(LivingMotions.DEATH, Animations.BIPED_DEATH), 0); + } + + public void addLivingAnimation(LivingMotion livingMotion, AssetAccessor animation) { + if (AnimationManager.checkNull(animation)) { + EpicFightMod.LOGGER.warn("Unable to put an empty animation for " + livingMotion); + return; + } + + this.livingAnimations.put(livingMotion, animation); + } + + public AssetAccessor getLivingAnimation(LivingMotion livingMotion, AssetAccessor defaultGetter) { + return this.livingAnimations.getOrDefault(livingMotion, defaultGetter); + } + + public Map> getLivingAnimations() { + return ImmutableMap.copyOf(this.livingAnimations); + } + + public AnimationVariables getVariables() { + return this.animationVariables; + } + + public LivingEntityPatch getEntityPatch() { + return this.entitypatch; + } + + public void resetLivingAnimations() { + this.livingAnimations.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/Keyframe.java b/src/main/java/com/tiedup/remake/rig/anim/Keyframe.java new file mode 100644 index 0000000..9c92023 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/Keyframe.java @@ -0,0 +1,47 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +public class Keyframe { + private float timeStamp; + private final JointTransform transform; + + public Keyframe(float timeStamp, JointTransform trasnform) { + this.timeStamp = timeStamp; + this.transform = trasnform; + } + + public Keyframe(Keyframe original) { + this.transform = JointTransform.empty(); + this.copyFrom(original); + } + + public void copyFrom(Keyframe target) { + this.timeStamp = target.timeStamp; + this.transform.copyFrom(target.transform); + } + + public float time() { + return this.timeStamp; + } + + public void setTime(float time) { + this.timeStamp = time; + } + + public JointTransform transform() { + return this.transform; + } + + public String toString() { + return "Keyframe[Time: " + this.timeStamp + ", " + (this.transform == null ? "null" : this.transform.toString()) + "]"; + } + + public static Keyframe empty() { + return new Keyframe(0.0F, JointTransform.empty()); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/LivingMotion.java b/src/main/java/com/tiedup/remake/rig/anim/LivingMotion.java new file mode 100644 index 0000000..b93a172 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/LivingMotion.java @@ -0,0 +1,24 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import com.tiedup.remake.rig.util.ExtendableEnum; +import com.tiedup.remake.rig.util.ExtendableEnumManager; + +public interface LivingMotion extends ExtendableEnum { + ExtendableEnumManager ENUM_MANAGER = new ExtendableEnumManager<> ("living_motion"); + + default boolean isSame(LivingMotion livingMotion) { + if (this == LivingMotions.IDLE && livingMotion == LivingMotions.INACTION) { + return true; + } else if (this == LivingMotions.INACTION && livingMotion == LivingMotions.IDLE) { + return true; + } + + return this == livingMotion; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/LivingMotions.java b/src/main/java/com/tiedup/remake/rig/anim/LivingMotions.java new file mode 100644 index 0000000..767156b --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/LivingMotions.java @@ -0,0 +1,23 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +public enum LivingMotions implements LivingMotion { + ALL, // Datapack edit option + INACTION, IDLE, CONFRONT, ANGRY, FLOAT, WALK, RUN, SWIM, FLY, SNEAK, KNEEL, FALL, SIT, MOUNT, DEATH, CHASE, SPELLCAST, JUMP, CELEBRATE, LANDING_RECOVERY, CREATIVE_FLY, CREATIVE_IDLE, SLEEP, // Base + DIGGING, ADMIRE, CLIMB, DRINK, EAT, NONE, AIM, BLOCK, BLOCK_SHIELD, RELOAD, SHOT, SPECTATE; // Mix + + final int id; + + LivingMotions() { + this.id = LivingMotion.ENUM_MANAGER.assign(this); + } + + public int universalOrdinal() { + return this.id; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/Pose.java b/src/main/java/com/tiedup/remake/rig/anim/Pose.java new file mode 100644 index 0000000..93eb1b4 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/Pose.java @@ -0,0 +1,113 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +import com.google.common.base.Predicate; +import com.google.common.collect.Maps; + +public class Pose { + public static final Pose EMPTY_POSE = new Pose(); + + public static Pose interpolatePose(Pose pose1, Pose pose2, float pregression) { + Pose pose = new Pose(); + + Set mergedSet = new HashSet<>(pose1.jointTransformData.keySet()); + mergedSet.addAll(pose2.jointTransformData.keySet()); + + for (String jointName : mergedSet) { + pose.putJointData(jointName, JointTransform.interpolate(pose1.orElseEmpty(jointName), pose2.orElseEmpty(jointName), pregression)); + } + + return pose; + } + + protected final Map jointTransformData; + + public Pose() { + this(Maps.newHashMap()); + } + + public Pose(Map jointTransforms) { + this.jointTransformData = jointTransforms; + } + + public void putJointData(String name, JointTransform transform) { + this.jointTransformData.put(name, transform); + } + + public Map getJointTransformData() { + return this.jointTransformData; + } + + public void disableJoint(Predicate> predicate) { + this.jointTransformData.entrySet().removeIf(predicate); + } + + public void disableAllJoints() { + this.jointTransformData.clear(); + } + + public boolean hasTransform(String jointName) { + return this.jointTransformData.containsKey(jointName); + } + + public JointTransform get(String jointName) { + return this.jointTransformData.get(jointName); + } + + public JointTransform orElseEmpty(String jointName) { + return this.jointTransformData.getOrDefault(jointName, JointTransform.empty()); + } + + public JointTransform orElse(String jointName, JointTransform orElse) { + return this.jointTransformData.getOrDefault(jointName, orElse); + } + + public void forEachEnabledTransforms(BiConsumer task) { + this.jointTransformData.forEach(task); + } + + public void load(Pose pose, LoadOperation operation) { + switch (operation) { + case SET -> { + this.disableAllJoints(); + pose.forEachEnabledTransforms(this::putJointData); + } + case OVERWRITE -> { + pose.forEachEnabledTransforms(this::putJointData); + } + case APPEND_ABSENT -> { + pose.forEachEnabledTransforms((name, transform) -> { + if (!this.hasTransform(name)) { + this.putJointData(name, transform); + } + }); + } + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Pose: "); + + for (Map.Entry entry : this.jointTransformData.entrySet()) { + sb.append(String.format("%s{%s, %s}, ", entry.getKey(), entry.getValue().translation().toString(), entry.getValue().rotation().toString()) + "\n"); + } + + return sb.toString(); + } + + public enum LoadOperation { + SET, OVERWRITE, APPEND_ABSENT + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/ServerAnimator.java b/src/main/java/com/tiedup/remake/rig/anim/ServerAnimator.java new file mode 100644 index 0000000..10c7d0b --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/ServerAnimator.java @@ -0,0 +1,160 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.Optional; + +import com.mojang.datafixers.util.Pair; + +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.types.EntityState; +import com.tiedup.remake.rig.anim.types.LinkAnimation; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import yesman.epicfight.gameasset.Animations; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class ServerAnimator extends Animator { + public static Animator getAnimator(LivingEntityPatch entitypatch) { + return new ServerAnimator(entitypatch); + } + + private final LinkAnimation linkAnimation; + public final AnimationPlayer animationPlayer; + + protected AssetAccessor nextAnimation; + public boolean hardPaused = false; + public boolean softPaused = false; + + public ServerAnimator(LivingEntityPatch entitypatch) { + super(entitypatch); + + this.linkAnimation = new LinkAnimation(); + this.animationPlayer = new AnimationPlayer(); + } + + /** Play an animation by animation instance **/ + @Override + public void playAnimation(AssetAccessor nextAnimation, float transitionTimeModifier) { + this.softPaused = false; + Pose lastPose = this.animationPlayer.getAnimation().get().getPoseByTime(this.entitypatch, 0.0F, 0.0F); + + if (!this.animationPlayer.isEnd()) { + this.animationPlayer.getAnimation().get().end(this.entitypatch, nextAnimation, false); + } + + nextAnimation.get().begin(this.entitypatch); + + if (!nextAnimation.get().isMetaAnimation()) { + nextAnimation.get().setLinkAnimation(this.animationPlayer.getAnimation(), lastPose, true, transitionTimeModifier, this.entitypatch, this.linkAnimation); + this.linkAnimation.getAnimationClip().setBaked(); + this.linkAnimation.putOnPlayer(this.animationPlayer, this.entitypatch); + this.entitypatch.updateEntityState(); + this.nextAnimation = nextAnimation; + } + } + + @Override + public void playAnimationInstantly(AssetAccessor nextAnimation) { + this.softPaused = false; + + if (!this.animationPlayer.isEnd()) { + this.animationPlayer.getAnimation().get().end(this.entitypatch, nextAnimation, false); + } + + nextAnimation.get().begin(this.entitypatch); + nextAnimation.get().putOnPlayer(this.animationPlayer, this.entitypatch); + this.entitypatch.updateEntityState(); + } + + @Override + public void reserveAnimation(AssetAccessor nextAnimation) { + this.softPaused = false; + this.nextAnimation = nextAnimation; + } + + @Override + public boolean stopPlaying(AssetAccessor targetAnimation) { + if (this.animationPlayer.getRealAnimation() == targetAnimation) { + this.animationPlayer.terminate(this.entitypatch); + return true; + } + + return false; + } + + @Override + public void playShootingAnimation() { + } + + @Override + public void tick() { + if (this.hardPaused || this.softPaused) { + this.entitypatch.updateEntityState(); + return; + } + + this.animationPlayer.tick(this.entitypatch); + this.entitypatch.updateEntityState(); + + if (this.animationPlayer.isEnd()) { + if (this.nextAnimation == null) { + Animations.EMPTY_ANIMATION.putOnPlayer(this.animationPlayer, this.entitypatch); + this.softPaused = true; + } else { + if (!this.animationPlayer.getAnimation().get().isLinkAnimation() && !this.nextAnimation.get().isLinkAnimation()) { + this.nextAnimation.get().begin(this.entitypatch); + } + + this.nextAnimation.get().putOnPlayer(this.animationPlayer, this.entitypatch); + this.nextAnimation = null; + } + } else { + this.animationPlayer.getAnimation().get().tick(this.entitypatch); + } + } + + @Override + public Pose getPose(float partialTicks) { + return this.animationPlayer.getCurrentPose(this.entitypatch, partialTicks); + } + + @Override + public AnimationPlayer getPlayerFor(AssetAccessor playingAnimation) { + return this.animationPlayer; + } + + @Override + public Optional getPlayer(AssetAccessor playingAnimation) { + if (this.animationPlayer.getRealAnimation() == playingAnimation.get().getRealAnimation()) { + return Optional.of(this.animationPlayer); + } else { + return Optional.empty(); + } + } + + @Override + @SuppressWarnings("unchecked") + public Pair findFor(Class animationType) { + return animationType.isAssignableFrom(this.animationPlayer.getAnimation().getClass()) ? Pair.of(this.animationPlayer, (T)this.animationPlayer.getAnimation()) : null; + } + + @Override + public EntityState getEntityState() { + return this.animationPlayer.getAnimation().get().getState(this.entitypatch, this.animationPlayer.getElapsedTime()); + } + + @Override + public void setSoftPause(boolean paused) { + this.softPaused = paused; + } + + @Override + public void setHardPause(boolean paused) { + this.hardPaused = paused; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/SynchedAnimationVariableKey.java b/src/main/java/com/tiedup/remake/rig/anim/SynchedAnimationVariableKey.java new file mode 100644 index 0000000..d9c88bb --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/SynchedAnimationVariableKey.java @@ -0,0 +1,130 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.function.Function; + +import javax.annotation.Nullable; + +import net.minecraft.core.IdMapper; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.registries.IForgeRegistry; +import net.minecraftforge.registries.IForgeRegistryInternal; +import net.minecraftforge.registries.RegistryManager; +import com.tiedup.remake.rig.anim.AnimationVariables.IndependentAnimationVariableKey; +import com.tiedup.remake.rig.anim.AnimationVariables.SharedAnimationVariableKey; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.util.PacketBufferCodec; +import com.tiedup.remake.rig.util.datastruct.ClearableIdMapper; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.network.EpicFightNetworkManager; +import yesman.epicfight.network.client.CPAnimationVariablePacket; +import yesman.epicfight.network.common.AnimationVariablePacket; +import yesman.epicfight.network.server.SPAnimationVariablePacket; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public interface SynchedAnimationVariableKey { + public static SynchedSharedAnimationVariableKey shared(Function defaultValueSupplier, boolean mutable, PacketBufferCodec codec) { + return new SynchedSharedAnimationVariableKey<> (defaultValueSupplier, mutable, codec); + } + + public static SynchedIndependentAnimationVariableKey independent(Function defaultValueSupplier, boolean mutable, PacketBufferCodec codec) { + return new SynchedIndependentAnimationVariableKey<> (defaultValueSupplier, mutable, codec); + } + + public static final ResourceLocation BY_ID_REGISTRY = EpicFightMod.identifier("variablekeytoid"); + + public static class SynchedAnimationVariableKeyCallbacks implements IForgeRegistry.BakeCallback>, IForgeRegistry.CreateCallback>, IForgeRegistry.ClearCallback> { + private static final SynchedAnimationVariableKeyCallbacks INSTANCE = new SynchedAnimationVariableKeyCallbacks(); + + @Override + @SuppressWarnings("unchecked") + public void onBake(IForgeRegistryInternal> owner, RegistryManager stage) { + final ClearableIdMapper> synchedanimationvariablekeybyid = owner.getSlaveMap(BY_ID_REGISTRY, ClearableIdMapper.class); + owner.forEach(synchedanimationvariablekeybyid::add); + } + + @Override + public void onCreate(IForgeRegistryInternal> owner, RegistryManager stage) { + owner.setSlaveMap(BY_ID_REGISTRY, new ClearableIdMapper> (owner.getKeys().size())); + } + + @Override + public void onClear(IForgeRegistryInternal> owner, RegistryManager stage) { + owner.getSlaveMap(BY_ID_REGISTRY, ClearableIdMapper.class).clear(); + } + } + + public static SynchedAnimationVariableKeyCallbacks getRegistryCallback() { + return SynchedAnimationVariableKeyCallbacks.INSTANCE; + } + + @SuppressWarnings("unchecked") + public static IdMapper> getIdMap() { + return SynchedAnimationVariableKeys.REGISTRY.get().getSlaveMap(BY_ID_REGISTRY, IdMapper.class); + } + + @SuppressWarnings("unchecked") + public static SynchedAnimationVariableKey byId(int id) { + return (SynchedAnimationVariableKey)getIdMap().byId(id); + } + + public PacketBufferCodec getPacketBufferCodec(); + + public boolean isSharedKey(); + + default int getId() { + return getIdMap().getId(this); + } + + default void sync(LivingEntityPatch entitypatch, @Nullable AssetAccessor animation, T value, AnimationVariablePacket.Action action) { + if (entitypatch.isLogicalClient()) { + EpicFightNetworkManager.sendToServer(new CPAnimationVariablePacket<> (this, animation, value, action)); + } else { + entitypatch.sendToAllPlayersTrackingMe(new SPAnimationVariablePacket<> (entitypatch, this, animation, value, action)); + } + } + + public static class SynchedSharedAnimationVariableKey extends SharedAnimationVariableKey implements SynchedAnimationVariableKey { + private final PacketBufferCodec packetBufferCodec; + + protected SynchedSharedAnimationVariableKey(Function defaultValueSupplier, boolean mutable, PacketBufferCodec packetBufferCodec) { + super(defaultValueSupplier, mutable); + this.packetBufferCodec = packetBufferCodec; + } + + @Override + public boolean isSynched() { + return true; + } + + @Override + public PacketBufferCodec getPacketBufferCodec() { + return this.packetBufferCodec; + } + } + + public static class SynchedIndependentAnimationVariableKey extends IndependentAnimationVariableKey implements SynchedAnimationVariableKey { + private final PacketBufferCodec packetBufferCodec; + + protected SynchedIndependentAnimationVariableKey(Function defaultValueSupplier, boolean mutable, PacketBufferCodec packetBufferCodec) { + super(defaultValueSupplier, mutable); + this.packetBufferCodec = packetBufferCodec; + } + + @Override + public boolean isSharedKey() { + return false; + } + + @Override + public PacketBufferCodec getPacketBufferCodec() { + return this.packetBufferCodec; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/SynchedAnimationVariableKeys.java b/src/main/java/com/tiedup/remake/rig/anim/SynchedAnimationVariableKeys.java new file mode 100644 index 0000000..7fe87d2 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/SynchedAnimationVariableKeys.java @@ -0,0 +1,35 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.function.Supplier; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.IForgeRegistry; +import net.minecraftforge.registries.RegistryBuilder; +import net.minecraftforge.registries.RegistryObject; +import com.tiedup.remake.rig.anim.SynchedAnimationVariableKey.SynchedIndependentAnimationVariableKey; +import com.tiedup.remake.rig.util.PacketBufferCodec; +import yesman.epicfight.main.EpicFightMod; + +public class SynchedAnimationVariableKeys { + private static final Supplier>> BUILDER = () -> new RegistryBuilder>().addCallback(SynchedAnimationVariableKey.getRegistryCallback()); + + public static final DeferredRegister> SYNCHED_ANIMATION_VARIABLE_KEYS = DeferredRegister.create(EpicFightMod.identifier("synched_animation_variable_keys"), EpicFightMod.MODID); + public static final Supplier>> REGISTRY = SYNCHED_ANIMATION_VARIABLE_KEYS.makeRegistry(BUILDER); + + public static final RegistryObject> DESTINATION = SYNCHED_ANIMATION_VARIABLE_KEYS.register("destination", () -> + SynchedAnimationVariableKey.independent(animator -> animator.getEntityPatch().getOriginal().position(), true, PacketBufferCodec.VEC3)); + + public static final RegistryObject> TARGET_ENTITY = SYNCHED_ANIMATION_VARIABLE_KEYS.register("target_entity", () -> + SynchedAnimationVariableKey.independent(animator -> (Integer)null, true, PacketBufferCodec.INTEGER)); + + public static final RegistryObject> CHARGING_TICKS = SYNCHED_ANIMATION_VARIABLE_KEYS.register("animation_playing_speed", () -> + SynchedAnimationVariableKey.independent(animator -> 0, true, PacketBufferCodec.INTEGER)); +} diff --git a/src/main/java/com/tiedup/remake/rig/anim/TransformSheet.java b/src/main/java/com/tiedup/remake/rig/anim/TransformSheet.java new file mode 100644 index 0000000..180a81b --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/TransformSheet.java @@ -0,0 +1,354 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.joml.Quaternionf; + +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import com.tiedup.remake.rig.math.MathUtils; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class TransformSheet { + public static final TransformSheet EMPTY_SHEET = new TransformSheet(List.of(new Keyframe(0.0F, JointTransform.empty()), new Keyframe(Float.MAX_VALUE, JointTransform.empty()))); + public static final Function EMPTY_SHEET_PROVIDER = translation -> { + return new TransformSheet(List.of(new Keyframe(0.0F, JointTransform.translation(new Vec3f(translation))), new Keyframe(Float.MAX_VALUE, JointTransform.empty()))); + }; + + private Keyframe[] keyframes; + + public TransformSheet() { + this(new Keyframe[0]); + } + + public TransformSheet(int size) { + this(new Keyframe[size]); + } + + public TransformSheet(List keyframeList) { + this(keyframeList.toArray(new Keyframe[0])); + } + + public TransformSheet(Keyframe[] keyframes) { + this.keyframes = keyframes; + } + + public JointTransform getStartTransform() { + return this.keyframes[0].transform(); + } + + public Keyframe[] getKeyframes() { + return this.keyframes; + } + + public TransformSheet copyAll() { + return this.copy(0, this.keyframes.length); + } + + public TransformSheet copy(int start, int end) { + int len = end - start; + Keyframe[] newKeyframes = new Keyframe[len]; + + for (int i = 0; i < len; i++) { + Keyframe kf = this.keyframes[i + start]; + newKeyframes[i] = new Keyframe(kf); + } + + return new TransformSheet(newKeyframes); + } + + public TransformSheet readFrom(TransformSheet opponent) { + if (opponent.keyframes.length != this.keyframes.length) { + this.keyframes = new Keyframe[opponent.keyframes.length]; + + for (int i = 0; i < this.keyframes.length; i++) { + this.keyframes[i] = Keyframe.empty(); + } + } + + for (int i = 0; i < this.keyframes.length; i++) { + this.keyframes[i].copyFrom(opponent.keyframes[i]); + } + + return this; + } + + public TransformSheet createInterpolated(float[] timestamp) { + TransformSheet interpolationCreated = new TransformSheet(timestamp.length); + + for (int i = 0; i < timestamp.length; i++) { + interpolationCreated.keyframes[i] = new Keyframe(timestamp[i], this.getInterpolatedTransform(timestamp[i])); + } + + return interpolationCreated; + } + + /** + * Transform each joint + */ + public void forEach(BiConsumer task) { + this.forEach(task, 0, this.keyframes.length); + } + + public void forEach(BiConsumer task, int start, int end) { + end = Math.min(end, this.keyframes.length); + + for (int i = start; i < end; i++) { + task.accept(i, this.keyframes[i]); + } + } + + public Vec3f getInterpolatedTranslation(float currentTime) { + InterpolationInfo interpolInfo = this.getInterpolationInfo(currentTime); + + if (interpolInfo == InterpolationInfo.INVALID) { + return new Vec3f(); + } + + Vec3f vec3f = MathUtils.lerpVector(this.keyframes[interpolInfo.prev].transform().translation(), this.keyframes[interpolInfo.next].transform().translation(), interpolInfo.delta); + return vec3f; + } + + public Quaternionf getInterpolatedRotation(float currentTime) { + InterpolationInfo interpolInfo = this.getInterpolationInfo(currentTime); + + if (interpolInfo == InterpolationInfo.INVALID) { + return new Quaternionf(); + } + + Quaternionf quat = MathUtils.lerpQuaternion(this.keyframes[interpolInfo.prev].transform().rotation(), this.keyframes[interpolInfo.next].transform().rotation(), interpolInfo.delta); + return quat; + } + + public JointTransform getInterpolatedTransform(float currentTime) { + return this.getInterpolatedTransform(this.getInterpolationInfo(currentTime)); + } + + public JointTransform getInterpolatedTransform(InterpolationInfo interpolationInfo) { + if (interpolationInfo == InterpolationInfo.INVALID) { + return JointTransform.empty(); + } + + JointTransform trasnform = JointTransform.interpolate(this.keyframes[interpolationInfo.prev].transform(), this.keyframes[interpolationInfo.next].transform(), interpolationInfo.delta); + return trasnform; + } + + public TransformSheet extend(TransformSheet target) { + int newKeyLength = this.keyframes.length + target.keyframes.length; + Keyframe[] newKeyfrmaes = new Keyframe[newKeyLength]; + + for (int i = 0; i < this.keyframes.length; i++) { + newKeyfrmaes[i] = this.keyframes[i]; + } + + for (int i = this.keyframes.length; i < newKeyLength; i++) { + newKeyfrmaes[i] = new Keyframe(target.keyframes[i - this.keyframes.length]); + } + + this.keyframes = newKeyfrmaes; + + return this; + } + + public TransformSheet getFirstFrame() { + TransformSheet part = this.copy(0, 2); + Keyframe[] keyframes = part.getKeyframes(); + keyframes[1].transform().copyFrom(keyframes[0].transform()); + + return part; + } + + public void correctAnimationByNewPosition(Vec3f startpos, Vec3f startToEnd, Vec3f modifiedStart, Vec3f modifiedStartToEnd) { + Keyframe[] keyframes = this.getKeyframes(); + Keyframe startKeyframe = keyframes[0]; + Keyframe endKeyframe = keyframes[keyframes.length - 1]; + float pitchDeg = (float) Math.toDegrees(Mth.atan2(modifiedStartToEnd.y - startToEnd.y, modifiedStartToEnd.length())); + float yawDeg = (float) MathUtils.getAngleBetween(modifiedStartToEnd.copy().multiply(1.0F, 0.0F, 1.0F), startToEnd.copy().multiply(1.0F, 0.0F, 1.0F)); + + for (Keyframe kf : keyframes) { + float lerp = (kf.time() - startKeyframe.time()) / (endKeyframe.time() - startKeyframe.time()); + Vec3f line = MathUtils.lerpVector(new Vec3f(0F, 0F, 0F), startToEnd, lerp); + Vec3f modifiedLine = MathUtils.lerpVector(new Vec3f(0F, 0F, 0F), modifiedStartToEnd, lerp); + Vec3f keyTransform = kf.transform().translation(); + Vec3f startToKeyTransform = keyTransform.copy().sub(startpos).multiply(-1.0F, 1.0F, -1.0F); + Vec3f animOnLine = startToKeyTransform.copy().sub(line); + OpenMatrix4f rotator = OpenMatrix4f.createRotatorDeg(pitchDeg, Vec3f.X_AXIS).mulFront(OpenMatrix4f.createRotatorDeg(yawDeg, Vec3f.Y_AXIS)); + Vec3f toNewKeyTransform = modifiedLine.add(OpenMatrix4f.transform3v(rotator, animOnLine, null)); + keyTransform.set(modifiedStart.copy().add((toNewKeyTransform))); + } + } + + public TransformSheet getCorrectedModelCoord(LivingEntityPatch entitypatch, Vec3 start, Vec3 dest, int startFrame, int endFrame) { + TransformSheet transform = this.copyAll(); + float horizontalDistance = (float) dest.subtract(start).horizontalDistance(); + float verticalDistance = (float) Math.abs(dest.y - start.y); + JointTransform startJt = transform.getKeyframes()[startFrame].transform(); + JointTransform endJt = transform.getKeyframes()[endFrame].transform(); + Vec3f jointCoord = new Vec3f(startJt.translation().x, verticalDistance, horizontalDistance); + + startJt.translation().set(jointCoord); + + for (int i = startFrame + 1; i < endFrame; i++) { + JointTransform middleJt = transform.getKeyframes()[i].transform(); + middleJt.translation().set(MathUtils.lerpVector(startJt.translation(), endJt.translation(), transform.getKeyframes()[i].time() / transform.getKeyframes()[endFrame].time())); + } + + return transform; + } + + public TransformSheet extendsZCoord(float multiplier, int startFrame, int endFrame) { + TransformSheet transform = this.copyAll(); + float extend = 0.0F; + + for (int i = 0; i < endFrame + 1; i++) { + Keyframe kf = transform.getKeyframes()[i]; + float prevZ = kf.transform().translation().z; + kf.transform().translation().multiply(1.0F, 1.0F, multiplier); + float extendedZ = kf.transform().translation().z; + extend = extendedZ - prevZ; + } + + for (int i = endFrame + 1; i < transform.getKeyframes().length; i++) { + Keyframe kf = transform.getKeyframes()[i]; + kf.transform().translation().add(0.0F, 0.0F, extend); + } + + return transform; + } + + /** + * Transform the animation coord system to world coord system regarding origin point as @param worldDest + * + * @param entitypatch + * @param worldStart + * @param worldDest + * @param xRot + * @param entityYRot + * @param startFrame + * @param endFrame + * @return + */ + public TransformSheet transformToWorldCoordOriginAsDest(LivingEntityPatch entitypatch, Vec3 startInWorld, Vec3 destInWorld, float entityYRot, float destYRot, int startFrmae, int destFrame) { + TransformSheet byStart = this.copy(0, destFrame + 1); + TransformSheet byDest = this.copy(0, destFrame + 1); + TransformSheet result = new TransformSheet(destFrame + 1); + Vec3 toTargetInWorld = destInWorld.subtract(startInWorld); + double worldMagnitude = toTargetInWorld.horizontalDistance(); + double animMagnitude = this.keyframes[0].transform().translation().horizontalDistance(); + float scale = (float)(worldMagnitude / animMagnitude); + + byStart.forEach((idx, keyframe) -> { + keyframe.transform().translation().sub(this.keyframes[0].transform().translation()); + keyframe.transform().translation().multiply(1.0F, 1.0F, scale); + keyframe.transform().translation().rotate(-entityYRot, Vec3f.Y_AXIS); + keyframe.transform().translation().multiply(-1.0F, 1.0F, -1.0F); + keyframe.transform().translation().add(startInWorld); + }); + + byDest.forEach((idx, keyframe) -> { + keyframe.transform().translation().multiply(1.0F, 1.0F, Mth.lerp((idx / (float)destFrame), scale, 1.0F)); + keyframe.transform().translation().rotate(-destYRot, Vec3f.Y_AXIS); + keyframe.transform().translation().multiply(-1.0F, 1.0F, -1.0F); + keyframe.transform().translation().add(destInWorld); + }); + + for (int i = 0; i < destFrame + 1; i++) { + if (i <= startFrmae) { + result.getKeyframes()[i] = new Keyframe(this.keyframes[i].time(), JointTransform.translation(byStart.getKeyframes()[i].transform().translation())); + } else { + float lerp = this.keyframes[i].time() == 0.0F ? 0.0F : this.keyframes[i].time() / this.keyframes[destFrame].time(); + Vec3f lerpTranslation = Vec3f.interpolate(byStart.getKeyframes()[i].transform().translation(), byDest.getKeyframes()[i].transform().translation(), lerp, null); + result.getKeyframes()[i] = new Keyframe(this.keyframes[i].time(), JointTransform.translation(lerpTranslation)); + } + } + + if (this.keyframes.length > destFrame) { + TransformSheet behindDestination = this.copy(destFrame + 1, this.keyframes.length); + + behindDestination.forEach((idx, keyframe) -> { + keyframe.transform().translation().sub(this.keyframes[destFrame].transform().translation()); + keyframe.transform().translation().rotate(entityYRot, Vec3f.Y_AXIS); + keyframe.transform().translation().multiply(-1.0F, 1.0F, -1.0F); + keyframe.transform().translation().add(result.getKeyframes()[destFrame].transform().translation()); + }); + + result.extend(behindDestination); + } + + return result; + } + + public InterpolationInfo getInterpolationInfo(float currentTime) { + if (this.keyframes.length == 0) { + return InterpolationInfo.INVALID; + } + + if (currentTime < 0.0F) { + currentTime = this.keyframes[this.keyframes.length - 1].time() + currentTime; + } + + // Binary search + int begin = 0, end = this.keyframes.length - 1; + + while (end - begin > 1) { + int i = begin + (end - begin) / 2; + + if (this.keyframes[i].time() <= currentTime && this.keyframes[i+1].time() > currentTime) { + begin = i; + end = i+1; + break; + } else { + if (this.keyframes[i].time() > currentTime) { + end = i; + } else if (this.keyframes[i+1].time() <= currentTime) { + begin = i; + } + } + } + + float progression = Mth.clamp((currentTime - this.keyframes[begin].time()) / (this.keyframes[end].time() - this.keyframes[begin].time()), 0.0F, 1.0F); + return new InterpolationInfo(begin, end, Float.isNaN(progression) ? 1.0F : progression); + } + + public float maxFrameTime() { + float maxFrameTime = -1.0F; + + for (Keyframe kf : this.keyframes) { + if (kf.time() > maxFrameTime) { + maxFrameTime = kf.time(); + } + } + + return maxFrameTime; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + int idx = 0; + + for (Keyframe kf : this.keyframes) { + sb.append(kf); + + if (++idx < this.keyframes.length) { + sb.append("\n"); + } + } + + return sb.toString(); + } + + public static record InterpolationInfo(int prev, int next, float delta) { + public static final InterpolationInfo INVALID = new InterpolationInfo(-1, -1, -1.0F); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/client/AnimationSubFileReader.java b/src/main/java/com/tiedup/remake/rig/anim/client/AnimationSubFileReader.java new file mode 100644 index 0000000..e3eb0dd --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/client/AnimationSubFileReader.java @@ -0,0 +1,321 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonReader; +import com.mojang.datafixers.util.Pair; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.util.GsonHelper; +import com.tiedup.remake.rig.anim.AnimationManager; +import com.tiedup.remake.rig.anim.LivingMotion; +import com.tiedup.remake.rig.anim.TransformSheet; +import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty; +import com.tiedup.remake.rig.anim.types.ActionAnimation; +import com.tiedup.remake.rig.anim.types.DirectStaticAnimation; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.JsonAssetLoader; +import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties; +import com.tiedup.remake.rig.anim.client.property.JointMaskEntry; +import com.tiedup.remake.rig.anim.client.property.JointMaskReloadListener; +import com.tiedup.remake.rig.anim.client.property.LayerInfo; +import com.tiedup.remake.rig.anim.client.property.TrailInfo; +import com.tiedup.remake.rig.exception.AssetLoadingException; +import com.tiedup.remake.rig.util.ParseUtil; +import yesman.epicfight.main.EpicFightMod; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class AnimationSubFileReader { + public static final SubFileType SUBFILE_CLIENT_PROPERTY = new ClientPropertyType(); + public static final SubFileType SUBFILE_POV_ANIMATION = new PovAnimationType(); + + public static void readAndApply(StaticAnimation animation, Resource iresource, SubFileType subFileType) { + InputStream inputstream = null; + + try { + inputstream = iresource.open(); + } catch (IOException e) { + e.printStackTrace(); + } + + assert inputstream != null : "Input stream is null"; + + try { + subFileType.apply(inputstream, animation); + } catch (JsonParseException e) { + EpicFightMod.LOGGER.warn("Can't read sub file " + subFileType.directory + " for " + animation); + e.printStackTrace(); + } + } + + public static abstract class SubFileType { + private final String directory; + private final AnimationSubFileDeserializer deserializer; + + private SubFileType(String directory, AnimationSubFileDeserializer deserializer) { + this.directory = directory; + this.deserializer = deserializer; + } + + // Deserialize from input stream + public void apply(InputStream inputstream, StaticAnimation animation) { + Reader reader = new InputStreamReader(inputstream, StandardCharsets.UTF_8); + JsonReader jsonReader = new JsonReader(reader); + jsonReader.setLenient(true); + T deserialized = this.deserializer.deserialize(animation, Streams.parse(jsonReader)); + this.applySubFileInfo(deserialized, animation); + } + + // Deserialize from json object + public void apply(JsonElement jsonElement, StaticAnimation animation) { + T deserialized = this.deserializer.deserialize(animation, jsonElement); + this.applySubFileInfo(deserialized, animation); + } + + protected abstract void applySubFileInfo(T deserialized, StaticAnimation animation); + + public String getDirectory() { + return this.directory; + } + } + + private record ClientProperty(LayerInfo layerInfo, LayerInfo multilayerInfo, List trailInfo) { + } + + private static class ClientPropertyType extends SubFileType { + private ClientPropertyType() { + super("data", new AnimationSubFileReader.ClientAnimationPropertyDeserializer()); + } + + @Override + public void applySubFileInfo(ClientProperty deserialized, StaticAnimation animation) { + if (deserialized.layerInfo() != null) { + if (deserialized.layerInfo().jointMaskEntry.isValid()) { + animation.addProperty(ClientAnimationProperties.JOINT_MASK, deserialized.layerInfo().jointMaskEntry); + } + + animation.addProperty(ClientAnimationProperties.LAYER_TYPE, deserialized.layerInfo().layerType); + animation.addProperty(ClientAnimationProperties.PRIORITY, deserialized.layerInfo().priority); + } + + if (deserialized.multilayerInfo() != null) { + DirectStaticAnimation multilayerAnimation = new DirectStaticAnimation(animation.getLocation(), animation.getTransitionTime(), animation.isRepeat(), animation.getRegistryName().toString() + "_multilayer", animation.getArmature()); + + if (deserialized.multilayerInfo().jointMaskEntry.isValid()) { + multilayerAnimation.addProperty(ClientAnimationProperties.JOINT_MASK, deserialized.multilayerInfo().jointMaskEntry); + } + + multilayerAnimation.addProperty(ClientAnimationProperties.LAYER_TYPE, deserialized.multilayerInfo().layerType); + multilayerAnimation.addProperty(ClientAnimationProperties.PRIORITY, deserialized.multilayerInfo().priority); + multilayerAnimation.addProperty(StaticAnimationProperty.ELAPSED_TIME_MODIFIER, (self, entitypatch, speed, prevElapsedTime, elapsedTime) -> { + Layer baseLayer = entitypatch.getClientAnimator().baseLayer; + + if (baseLayer.animationPlayer.getAnimation().get().getRealAnimation().get() != animation) { + return Pair.of(prevElapsedTime, elapsedTime); + } + + if (!self.isStaticAnimation() && baseLayer.animationPlayer.getAnimation().get().isStaticAnimation()) { + return Pair.of(prevElapsedTime + speed, elapsedTime + speed); + } + + return Pair.of(baseLayer.animationPlayer.getPrevElapsedTime(), baseLayer.animationPlayer.getElapsedTime()); + }); + + animation.addProperty(ClientAnimationProperties.MULTILAYER_ANIMATION, multilayerAnimation); + } + + if (deserialized.trailInfo().size() > 0) { + animation.addProperty(ClientAnimationProperties.TRAIL_EFFECT, deserialized.trailInfo()); + } + } + } + + private static class ClientAnimationPropertyDeserializer implements AnimationSubFileDeserializer { + private static LayerInfo deserializeLayerInfo(JsonObject jsonObject) { + return deserializeLayerInfo(jsonObject, null); + } + + private static LayerInfo deserializeLayerInfo(JsonObject jsonObject, @Nullable Layer.LayerType defaultLayerType) { + JointMaskEntry.Builder builder = JointMaskEntry.builder(); + Layer.Priority priority = jsonObject.has("priority") ? Layer.Priority.valueOf(GsonHelper.getAsString(jsonObject, "priority")) : null; + Layer.LayerType layerType = jsonObject.has("layer") ? Layer.LayerType.valueOf(GsonHelper.getAsString(jsonObject, "layer")) : Layer.LayerType.BASE_LAYER; + + if (jsonObject.has("masks")) { + JsonArray maskArray = jsonObject.get("masks").getAsJsonArray(); + + if (!maskArray.isEmpty()) { + builder.defaultMask(JointMaskReloadListener.getNoneMask()); + + maskArray.forEach(element -> { + JsonObject jointMaskEntry = element.getAsJsonObject(); + String livingMotionName = GsonHelper.getAsString(jointMaskEntry, "livingmotion"); + String type = GsonHelper.getAsString(jointMaskEntry, "type"); + + if (!type.contains(":")) { + type = (new StringBuilder(EpicFightMod.MODID)).append(":").append(type).toString(); + } + + if (livingMotionName.equals("ALL")) { + builder.defaultMask(JointMaskReloadListener.getJointMaskEntry(type)); + } else { + builder.mask((LivingMotion) LivingMotion.ENUM_MANAGER.getOrThrow(livingMotionName), JointMaskReloadListener.getJointMaskEntry(type)); + } + }); + } + } + + return new LayerInfo(builder.create(), priority, (defaultLayerType == null) ? layerType : defaultLayerType); + } + + @Override + public ClientProperty deserialize(StaticAnimation animation, JsonElement json) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + LayerInfo layerInfo = null; + LayerInfo multilayerInfo = null; + + if (jsonObject.has("multilayer")) { + JsonObject multiplayerJson = jsonObject.get("multilayer").getAsJsonObject(); + layerInfo = deserializeLayerInfo(multiplayerJson.get("base").getAsJsonObject()); + multilayerInfo = deserializeLayerInfo(multiplayerJson.get("composite").getAsJsonObject(), Layer.LayerType.COMPOSITE_LAYER); + } else { + layerInfo = deserializeLayerInfo(jsonObject); + } + + List trailInfos = Lists.newArrayList(); + + if (jsonObject.has("trail_effects")) { + JsonArray trailArray = jsonObject.get("trail_effects").getAsJsonArray(); + trailArray.forEach(element -> trailInfos.add(TrailInfo.deserialize(element))); + } + + return new ClientProperty(layerInfo, multilayerInfo, trailInfos); + } + } + + public static record PovSettings( + @Nullable TransformSheet cameraTransform, + Map visibilities, + RootTransformation rootTransformation, + @Nullable ViewLimit viewLimit, + boolean visibilityOthers, + boolean hasUniqueAnimation, + boolean syncFrame + ) { + public enum RootTransformation { + CAMERA, WORLD + } + + public record ViewLimit(float xRotMin, float xRotMax, float yRotMin, float yRotMax) { + } + } + + private static class PovAnimationType extends SubFileType { + private PovAnimationType() { + super("pov", new AnimationSubFileReader.PovAnimationDeserializer()); + } + + @Override + public void applySubFileInfo(PovSettings deserialized, StaticAnimation animation) { + ResourceLocation povAnimationLocation = deserialized.hasUniqueAnimation() ? AnimationManager.getSubAnimationFileLocation(animation.getLocation(), SUBFILE_POV_ANIMATION) : animation.getLocation(); + DirectStaticAnimation povAnimation = new DirectStaticAnimation(povAnimationLocation, animation.getTransitionTime(), animation.isRepeat(), animation.getRegistryName().toString() + "_pov", animation.getArmature()) { + @Override + public float getPlaySpeed(LivingEntityPatch entitypatch, DynamicAnimation pAnimation) { + return animation.getPlaySpeed(entitypatch, pAnimation); + } + }; + + animation.getProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER).ifPresent(speedModifier -> { + povAnimation.addProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER, speedModifier); + }); + + if (deserialized.syncFrame()) { + animation.getProperty(StaticAnimationProperty.ELAPSED_TIME_MODIFIER).ifPresent(elapsedTimeModifier -> { + povAnimation.addProperty(StaticAnimationProperty.ELAPSED_TIME_MODIFIER, elapsedTimeModifier); + }); + } + + animation.addProperty(ClientAnimationProperties.POV_ANIMATION, povAnimation); + animation.addProperty(ClientAnimationProperties.POV_SETTINGS, deserialized); + } + } + + private static class PovAnimationDeserializer implements AnimationSubFileDeserializer { + @Override + public PovSettings deserialize(StaticAnimation animation, JsonElement json) throws AssetLoadingException, JsonParseException { + JsonObject jObject = json.getAsJsonObject(); + TransformSheet cameraTransform = null; + PovSettings.ViewLimit viewLimit = null; + PovSettings.RootTransformation rootTrasnformation = null; + + if (jObject.has("root")) { + rootTrasnformation = PovSettings.RootTransformation.valueOf(ParseUtil.toUpperCase(GsonHelper.getAsString(jObject, "root"))); + } else { + if (animation instanceof ActionAnimation) { + rootTrasnformation = PovSettings.RootTransformation.WORLD; + } else { + rootTrasnformation = PovSettings.RootTransformation.CAMERA; + } + } + + if (jObject.has("camera")) { + JsonObject cameraTransformJObject = jObject.getAsJsonObject("camera"); + cameraTransform = JsonAssetLoader.getTransformSheet(cameraTransformJObject, null, false, JsonAssetLoader.TransformFormat.ATTRIBUTES); + } + + ImmutableMap.Builder visibilitiesBuilder = ImmutableMap.builder(); + boolean others = false; + + if (jObject.has("visibilities")) { + JsonObject visibilitiesObject = jObject.getAsJsonObject("visibilities"); + visibilitiesObject.entrySet().stream().filter((e) -> !"others".equals(e.getKey())).forEach((entry) -> visibilitiesBuilder.put(entry.getKey(), entry.getValue().getAsBoolean())); + others = visibilitiesObject.get("others").getAsBoolean(); + } else { + visibilitiesBuilder.put("leftArm", true); + visibilitiesBuilder.put("leftSleeve", true); + visibilitiesBuilder.put("rightArm", true); + visibilitiesBuilder.put("rightSleeve", true); + } + + if (jObject.has("limited_view_degrees")) { + JsonObject limitedViewDegrees = jObject.getAsJsonObject("limited_view_degrees"); + JsonArray xRot = limitedViewDegrees.get("xRot").getAsJsonArray(); + JsonArray yRot = limitedViewDegrees.get("yRot").getAsJsonArray(); + + float xRotMin = Math.min(xRot.get(0).getAsFloat(), xRot.get(1).getAsFloat()); + float xRotMax = Math.max(xRot.get(0).getAsFloat(), xRot.get(1).getAsFloat()); + float yRotMin = Math.min(yRot.get(0).getAsFloat(), yRot.get(1).getAsFloat()); + float yRotMax = Math.max(yRot.get(0).getAsFloat(), yRot.get(1).getAsFloat()); + viewLimit = new PovSettings.ViewLimit(xRotMin, xRotMax, yRotMin, yRotMax); + } + + return new PovSettings(cameraTransform, visibilitiesBuilder.build(), rootTrasnformation, viewLimit, others, jObject.has("animation"), GsonHelper.getAsBoolean(jObject, "sync_frame", false)); + } + } + + public interface AnimationSubFileDeserializer { + public T deserialize(StaticAnimation animation, JsonElement json) throws JsonParseException; + } +} diff --git a/src/main/java/com/tiedup/remake/rig/anim/client/ClientAnimator.java b/src/main/java/com/tiedup/remake/rig/anim/client/ClientAnimator.java new file mode 100644 index 0000000..4d397bc --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/client/ClientAnimator.java @@ -0,0 +1,611 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.client; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.jetbrains.annotations.ApiStatus; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.datafixers.util.Pair; + +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.util.Mth; +import com.tiedup.remake.rig.anim.AnimationManager; +import com.tiedup.remake.rig.anim.AnimationPlayer; +import com.tiedup.remake.rig.anim.Animator; +import com.tiedup.remake.rig.armature.Joint; +import com.tiedup.remake.rig.anim.LivingMotion; +import com.tiedup.remake.rig.anim.LivingMotions; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.ServerAnimator; +import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.types.EntityState; +import com.tiedup.remake.rig.anim.types.EntityState.StateFactor; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.anim.client.Layer.Priority; +import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties; +import com.tiedup.remake.rig.anim.client.property.JointMask.BindModifier; +import com.tiedup.remake.rig.anim.client.property.JointMask.JointMaskSet; +import com.tiedup.remake.rig.anim.client.property.JointMaskEntry; +import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap; +import yesman.epicfight.gameasset.Animations; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.network.common.AnimatorControlPacket; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class ClientAnimator extends Animator { + public static Animator getAnimator(LivingEntityPatch entitypatch) { + return entitypatch.isLogicalClient() ? new ClientAnimator(entitypatch) : ServerAnimator.getAnimator(entitypatch); + } + + private final Map> compositeLivingAnimations; + private final Map> defaultLivingAnimations; + private final Map> defaultCompositeLivingAnimations; + public final Layer.BaseLayer baseLayer; + private LivingMotion currentMotion; + private LivingMotion currentCompositeMotion; + private boolean hardPaused; + + public ClientAnimator(LivingEntityPatch entitypatch) { + this(entitypatch, Layer.BaseLayer::new); + } + + public ClientAnimator(LivingEntityPatch entitypatch, Supplier layerSupplier) { + super(entitypatch); + + this.currentMotion = LivingMotions.IDLE; + this.currentCompositeMotion = LivingMotions.IDLE; + this.compositeLivingAnimations = Maps.newHashMap(); + this.defaultLivingAnimations = Maps.newHashMap(); + this.defaultCompositeLivingAnimations = Maps.newHashMap(); + this.baseLayer = layerSupplier.get(); + } + + /** Play an animation by animation instance **/ + @Override + public void playAnimation(AssetAccessor nextAnimation, float transitionTimeModifier) { + Layer layer = nextAnimation.get().getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(nextAnimation.get().getPriority()); + layer.paused = false; + layer.playAnimation(nextAnimation, this.entitypatch, transitionTimeModifier); + } + + /** Play an animation with specifying layer and priority **/ + @ApiStatus.Internal + public void playAnimationAt(AssetAccessor nextAnimation, float transitionTimeModifier, AnimatorControlPacket.Layer layerType, AnimatorControlPacket.Priority priority) { + Layer layer = layerType == AnimatorControlPacket.Layer.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(AnimatorControlPacket.getPriority(priority)); + layer.paused = false; + layer.playAnimation(nextAnimation, this.entitypatch, transitionTimeModifier); + } + + @Override + public void playAnimationInstantly(AssetAccessor nextAnimation) { + Layer layer = nextAnimation.get().getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(nextAnimation.get().getPriority()); + layer.paused = false; + layer.playAnimationInstantly(nextAnimation, this.entitypatch); + } + + @Override + public void reserveAnimation(AssetAccessor nextAnimation) { + Layer layer = nextAnimation.get().getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(nextAnimation.get().getPriority()); + + if (nextAnimation.get().getPriority().isHigherThan(layer.animationPlayer.getRealAnimation().get().getPriority())) { + if (!layer.animationPlayer.isEnd() && layer.animationPlayer.getAnimation() != null) { + layer.animationPlayer.getAnimation().get().end(this.entitypatch, nextAnimation, false); + } + + layer.animationPlayer.terminate(this.entitypatch); + } + + layer.nextAnimation = nextAnimation; + layer.paused = false; + } + + @Override + public boolean stopPlaying(AssetAccessor targetAnimation) { + Layer layer = targetAnimation.get().getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(targetAnimation.get().getPriority()); + + if (layer.animationPlayer.getRealAnimation() == targetAnimation) { + layer.animationPlayer.terminate(this.entitypatch); + return true; + } + + return false; + } + + @Override + public void setSoftPause(boolean paused) { + this.iterAllLayers(layer -> layer.paused = paused); + } + + @Override + public void setHardPause(boolean paused) { + this.hardPaused = paused; + } + + @Override + public void addLivingAnimation(LivingMotion livingMotion, AssetAccessor animation) { + if (AnimationManager.checkNull(animation)) { + EpicFightMod.LOGGER.warn("Unable to put an empty animation for " + livingMotion); + return; + } + + Layer.LayerType layerType = animation.get().getLayerType(); + boolean isBaseLayer = (layerType == Layer.LayerType.BASE_LAYER); + + Map> storage = layerType == Layer.LayerType.BASE_LAYER ? this.livingAnimations : this.compositeLivingAnimations; + LivingMotion compareMotion = layerType == Layer.LayerType.BASE_LAYER ? this.currentMotion : this.currentCompositeMotion; + Layer layer = layerType == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(animation.get().getPriority()); + storage.put(livingMotion, animation); + + if (livingMotion == compareMotion) { + EntityState state = this.getEntityState(); + + if (!state.inaction()) { + layer.playLivingAnimation(animation, this.entitypatch); + } + } + + if (isBaseLayer) { + animation.get().getProperty(ClientAnimationProperties.MULTILAYER_ANIMATION).ifPresent(multilayerAnimation -> { + this.compositeLivingAnimations.put(livingMotion, multilayerAnimation); + + if (livingMotion == this.currentCompositeMotion) { + EntityState state = this.getEntityState(); + + if (!state.inaction()) { + layer.playLivingAnimation(multilayerAnimation, this.entitypatch); + } + } + }); + } + } + + public void setCurrentMotionsAsDefault() { + this.defaultLivingAnimations.putAll(this.livingAnimations); + this.defaultCompositeLivingAnimations.putAll(this.compositeLivingAnimations); + } + + @Override + public void resetLivingAnimations() { + super.resetLivingAnimations(); + this.compositeLivingAnimations.clear(); + this.defaultLivingAnimations.forEach((key, val) -> this.addLivingAnimation(key, val)); + this.defaultCompositeLivingAnimations.forEach((key, val) -> this.addLivingAnimation(key, val)); + } + + public AssetAccessor getLivingMotion(LivingMotion motion) { + return this.livingAnimations.getOrDefault(motion, this.livingAnimations.get(LivingMotions.IDLE)); + } + + public AssetAccessor getCompositeLivingMotion(LivingMotion motion) { + return this.compositeLivingAnimations.get(motion); + } + + @Override + public void postInit() { + super.postInit(); + + this.setCurrentMotionsAsDefault(); + + AssetAccessor idleMotion = this.livingAnimations.get(this.currentMotion); + this.baseLayer.playAnimationInstantly(idleMotion, this.entitypatch); + } + + @Override + public void tick() { + /** + // Layer debugging + for (Layer layer : this.getAllLayers()) { + System.out.println(layer); + } + System.out.println(); + **/ + + if (this.hardPaused) { + return; + } + + this.baseLayer.update(this.entitypatch); + + if (this.baseLayer.animationPlayer.isEnd() && this.baseLayer.nextAnimation == null && this.currentMotion != LivingMotions.DEATH) { + this.entitypatch.updateMotion(false); + + if (this.compositeLivingAnimations.containsKey(this.entitypatch.currentCompositeMotion)) { + this.playAnimation(this.getCompositeLivingMotion(this.entitypatch.currentCompositeMotion), 0.0F); + } + + this.baseLayer.playAnimation(this.getLivingMotion(this.entitypatch.currentLivingMotion), this.entitypatch, 0.0F); + } else { + if (!this.compareCompositeMotion(this.entitypatch.currentCompositeMotion)) { + /* Turns off the multilayer of the base layer */ + this.getLivingMotion(this.currentCompositeMotion).get().getProperty(ClientAnimationProperties.MULTILAYER_ANIMATION).ifPresent((multilayerAnimation) -> { + if (!this.compositeLivingAnimations.containsKey(this.entitypatch.currentCompositeMotion)) { + this.getCompositeLayer(multilayerAnimation.get().getPriority()).off(this.entitypatch); + } + }); + + if (this.compositeLivingAnimations.containsKey(this.currentCompositeMotion)) { + AssetAccessor nextLivingAnimation = this.getCompositeLivingMotion(this.entitypatch.currentCompositeMotion); + + if (nextLivingAnimation == null || nextLivingAnimation.get().getPriority() != this.getCompositeLivingMotion(this.currentCompositeMotion).get().getPriority()) { + this.getCompositeLayer(this.getCompositeLivingMotion(this.currentCompositeMotion).get().getPriority()).off(this.entitypatch); + } + } + + if (this.compositeLivingAnimations.containsKey(this.entitypatch.currentCompositeMotion)) { + this.playAnimation(this.getCompositeLivingMotion(this.entitypatch.currentCompositeMotion), 0.0F); + } + } + + if (!this.compareMotion(this.entitypatch.currentLivingMotion) && this.entitypatch.currentLivingMotion != LivingMotions.DEATH) { + if (this.livingAnimations.containsKey(this.entitypatch.currentLivingMotion)) { + this.baseLayer.playAnimation(this.getLivingMotion(this.entitypatch.currentLivingMotion), this.entitypatch, 0.0F); + } + } + } + + this.currentMotion = this.entitypatch.currentLivingMotion; + this.currentCompositeMotion = this.entitypatch.currentCompositeMotion; + } + + @Override + public void playDeathAnimation() { + if (!this.getPlayerFor(null).getAnimation().get().getProperty(ActionAnimationProperty.IS_DEATH_ANIMATION).orElse(false)) { + this.playAnimation(this.livingAnimations.getOrDefault(LivingMotions.DEATH, Animations.EMPTY_ANIMATION), 0.0F); + this.currentMotion = LivingMotions.DEATH; + } + } + + public AssetAccessor getJumpAnimation() { + return this.livingAnimations.get(LivingMotions.JUMP); + } + + public Layer getCompositeLayer(Layer.Priority priority) { + return this.baseLayer.compositeLayers.get(priority); + } + + public void renderDebuggingInfoForAllLayers(PoseStack poseStack, MultiBufferSource buffer, float partialTicks) { + this.iterAllLayers((layer) -> { + if (layer.isOff()) { + return; + } + + AnimationPlayer animPlayer = layer.animationPlayer; + float playTime = Mth.lerp(partialTicks, animPlayer.getPrevElapsedTime(), animPlayer.getElapsedTime()); + animPlayer.getAnimation().get().renderDebugging(poseStack, buffer, entitypatch, playTime, partialTicks); + }); + } + + public Collection getAllLayers() { + List layerList = Lists.newArrayList(); + layerList.add(this.baseLayer); + layerList.addAll(this.baseLayer.compositeLayers.values()); + + return layerList; + } + + /** + * Iterates all layers + * @param task + */ + public void iterAllLayers(Consumer task) { + task.accept(this.baseLayer); + this.baseLayer.compositeLayers.values().forEach(task); + } + + /** + * Iterate layers that is visible by priority + * @param task + * @return + */ + public void iterVisibleLayers(Consumer task) { + task.accept(this.baseLayer); + this.baseLayer.compositeLayers.values().stream() + .filter(layer -> layer.isDisabled() || layer.animationPlayer.isEmpty() || !layer.priority.isHigherOrEqual(this.baseLayer.baseLayerPriority)) + .forEach(task); + } + + /** + * Iterates all activated layers from the highest layer + * when base layer = highest, iterates only base layer + * when base layer = middle, iterates base layer and highest composite layer + * when base layer = lowest, iterates base layer and all composite layers + * + * @param task + * @return true if all layers didn't return false by @param task + */ + public boolean iterVisibleLayersUntilFalse(Function task) { + Layer.Priority[] highers = this.baseLayer.baseLayerPriority.highers(); + + for (int i = highers.length - 1; i >= 0; i--) { + Layer layer = this.baseLayer.getLayer(highers[i]); + + if (layer.isDisabled() || layer.animationPlayer.isEmpty()) { + if (highers[i] == this.baseLayer.baseLayerPriority) { + return task.apply(this.baseLayer); + } + + continue; + } + + if (!task.apply(layer)) { + return false; + } + + if (highers[i] == this.baseLayer.baseLayerPriority) { + return task.apply(this.baseLayer); + } + } + + return true; + } + + @Override + public Pose getPose(float partialTicks) { + return this.getPose(partialTicks, true); + } + + public Pose getPose(float partialTicks, boolean useCurrentMotion) { + Pose composedPose = new Pose(); + Pose baseLayerPose = this.baseLayer.getEnabledPose(this.entitypatch, useCurrentMotion, partialTicks); + + Map, Pose>> layerPoses = Maps.newLinkedHashMap(); + composedPose.load(baseLayerPose, Pose.LoadOperation.OVERWRITE); + + for (Layer.Priority priority : this.baseLayer.baseLayerPriority.highers()) { + Layer compositeLayer = this.baseLayer.compositeLayers.get(priority); + + if (!compositeLayer.isDisabled() && !compositeLayer.animationPlayer.isEmpty()) { + Pose layerPose = compositeLayer.getEnabledPose(this.entitypatch, useCurrentMotion, partialTicks); + layerPoses.put(priority, Pair.of(compositeLayer.animationPlayer.getAnimation(), layerPose)); + composedPose.load(layerPose, Pose.LoadOperation.OVERWRITE); + } + } + + Joint rootJoint = this.entitypatch.getArmature().rootJoint; + this.applyBindModifier(baseLayerPose, composedPose, rootJoint, layerPoses, useCurrentMotion); + + return composedPose; + } + + public Pose getComposedLayerPoseBelow(Layer.Priority priorityLimit, float partialTicks) { + Pose composedPose = this.baseLayer.getEnabledPose(this.entitypatch, true, partialTicks); + Pose baseLayerPose = this.baseLayer.getEnabledPose(this.entitypatch, true, partialTicks); + Map, Pose>> layerPoses = Maps.newLinkedHashMap(); + + for (Layer.Priority priority : priorityLimit.lowers()) { + Layer compositeLayer = this.baseLayer.compositeLayers.get(priority); + + if (!compositeLayer.isDisabled()) { + Pose layerPose = compositeLayer.getEnabledPose(this.entitypatch, true, partialTicks); + layerPoses.put(priority, Pair.of(compositeLayer.animationPlayer.getAnimation(), layerPose)); + composedPose.load(layerPose, Pose.LoadOperation.OVERWRITE); + } + } + + if (!layerPoses.isEmpty()) { + this.applyBindModifier(baseLayerPose, composedPose, this.entitypatch.getArmature().rootJoint, layerPoses, true); + } + + return composedPose; + } + + public void applyBindModifier(Pose basePose, Pose result, Joint joint, Map, Pose>> poses, boolean useCurrentMotion) { + List list = Lists.newArrayList(poses.keySet()); + Collections.reverse(list); + + for (Layer.Priority priority : list) { + AssetAccessor nowPlaying = poses.get(priority).getFirst(); + JointMaskEntry jointMaskEntry = nowPlaying.get().getJointMaskEntry(this.entitypatch, useCurrentMotion).orElse(null); + + if (jointMaskEntry != null) { + LivingMotion livingMotion = this.getCompositeLayer(priority).getLivingMotion(this.entitypatch, useCurrentMotion); + + if (nowPlaying.get().hasTransformFor(joint.getName()) && !jointMaskEntry.isMasked(livingMotion, joint.getName())) { + JointMaskSet jointmaskset = jointMaskEntry.getMask(livingMotion); + BindModifier bindModifier = jointmaskset.getBindModifier(joint.getName()); + + if (bindModifier != null) { + bindModifier.modify(this.entitypatch, basePose, result, livingMotion, jointMaskEntry, priority, joint, poses); + break; + } + } + } + } + + for (Joint subJoints : joint.getSubJoints()) { + this.applyBindModifier(basePose, result, subJoints, poses, useCurrentMotion); + } + } + + public boolean compareMotion(LivingMotion motion) { + return this.currentMotion.isSame(motion); + } + + public boolean compareCompositeMotion(LivingMotion motion) { + return this.currentCompositeMotion.isSame(motion); + } + + public void forceResetBeforeAction(LivingMotion livingMotion, LivingMotion compositeLivingMotion) { + if (!this.currentMotion.equals(livingMotion)) { + if (this.livingAnimations.containsKey(livingMotion)) { + this.baseLayer.playAnimation(this.getLivingMotion(livingMotion), this.entitypatch, 0.0F); + } + } + + this.entitypatch.currentLivingMotion = livingMotion; + this.currentMotion = livingMotion; + + if (!this.currentCompositeMotion.equals(compositeLivingMotion)) { + if (this.compositeLivingAnimations.containsKey(this.currentCompositeMotion)) { + this.getCompositeLayer(this.getCompositeLivingMotion(this.currentCompositeMotion).get().getPriority()).off(this.entitypatch); + } + + if (this.compositeLivingAnimations.containsKey(compositeLivingMotion)) { + this.playAnimation(this.getCompositeLivingMotion(compositeLivingMotion), 0.0F); + } + } + + this.currentCompositeMotion = LivingMotions.NONE; + this.entitypatch.currentCompositeMotion = LivingMotions.NONE; + } + + public void resetMotion(boolean resetPrevMotion) { + if (resetPrevMotion) this.currentMotion = LivingMotions.IDLE; + this.entitypatch.currentLivingMotion = LivingMotions.IDLE; + } + + public void resetCompositeMotion() { + if (this.currentCompositeMotion != LivingMotions.IDLE && this.compositeLivingAnimations.containsKey(this.currentCompositeMotion)) { + AssetAccessor currentPlaying = this.getCompositeLivingMotion(this.currentCompositeMotion); + AssetAccessor resetPlaying = this.getCompositeLivingMotion(LivingMotions.IDLE); + + if (resetPlaying != null && currentPlaying != resetPlaying) { + this.playAnimation(resetPlaying, 0.0F); + } else if (currentPlaying != null) { + this.getCompositeLayer(currentPlaying.get().getPriority()).off(this.entitypatch); + } + } + + this.currentCompositeMotion = LivingMotions.NONE; + this.entitypatch.currentCompositeMotion = LivingMotions.NONE; + } + + public void offAllLayers() { + for (Layer layer : this.baseLayer.compositeLayers.values()) { + layer.off(this.entitypatch); + } + } + + public boolean isAiming() { + return this.currentCompositeMotion == LivingMotions.AIM; + } + + @Override + public void playShootingAnimation() { + if (this.compositeLivingAnimations.containsKey(LivingMotions.SHOT)) { + this.playAnimation(this.compositeLivingAnimations.get(LivingMotions.SHOT), 0.0F); + this.entitypatch.currentCompositeMotion = LivingMotions.NONE; + this.currentCompositeMotion = LivingMotions.NONE; + } + } + + @Override + public AnimationPlayer getPlayerFor(AssetAccessor playingAnimation) { + if (playingAnimation == null) { + return this.baseLayer.animationPlayer; + } + + DynamicAnimation animation = playingAnimation.get(); + + if (animation instanceof StaticAnimation staticAnimation) { + Layer layer = staticAnimation.getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(staticAnimation.getPriority()); + if (layer.animationPlayer.getAnimation() == playingAnimation) return layer.animationPlayer; + } + + for (Layer layer : this.baseLayer.compositeLayers.values()) { + if (layer.animationPlayer.getRealAnimation().equals(playingAnimation)) { + return layer.animationPlayer; + } + } + + return this.baseLayer.animationPlayer; + } + + @Override + public Optional getPlayer(AssetAccessor playingAnimation) { + DynamicAnimation animation = playingAnimation.get(); + + if (animation instanceof StaticAnimation staticAnimation) { + Layer layer = staticAnimation.getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(staticAnimation.getPriority()); + + if (layer.animationPlayer.getRealAnimation().equals(playingAnimation)) { + return Optional.of(layer.animationPlayer); + } + } + + if (this.baseLayer.animationPlayer.getRealAnimation().equals(playingAnimation.get().getRealAnimation())) { + return Optional.of(this.baseLayer.animationPlayer); + } + + for (Layer layer : this.baseLayer.compositeLayers.values()) { + if (layer.animationPlayer.getRealAnimation().equals(playingAnimation.get().getRealAnimation())) { + return Optional.of(layer.animationPlayer); + } + } + + return Optional.empty(); + } + + public Layer.Priority getPriorityFor(AssetAccessor playingAnimation) { + for (Layer layer : this.baseLayer.compositeLayers.values()) { + if (layer.animationPlayer.getRealAnimation().equals(playingAnimation)) { + return layer.priority; + } + } + + return this.baseLayer.priority; + } + + public LivingMotion currentMotion() { + return this.currentMotion; + } + + public LivingMotion currentCompositeMotion() { + return this.currentCompositeMotion; + } + + @SuppressWarnings("unchecked") + @Override + public Pair findFor(Class animationType) { + for (Layer layer : this.baseLayer.compositeLayers.values()) { + if (animationType.isAssignableFrom(layer.animationPlayer.getAnimation().getClass())) { + return Pair.of(layer.animationPlayer, (T)layer.animationPlayer.getAnimation()); + } + } + + return animationType.isAssignableFrom(this.baseLayer.animationPlayer.getAnimation().getClass()) ? Pair.of(this.baseLayer.animationPlayer, (T)this.baseLayer.animationPlayer.getAnimation()) : null; + } + + public LivingEntityPatch getOwner() { + return this.entitypatch; + } + + @Override + public EntityState getEntityState() { + TypeFlexibleHashMap> stateMap = new TypeFlexibleHashMap<> (false); + + for (Layer layer : this.baseLayer.compositeLayers.values()) { + if (this.baseLayer.baseLayerPriority.isHigherThan(layer.priority)) { + continue; + } + + if (!layer.isOff()) { + stateMap.putAll(layer.animationPlayer.getAnimation().get().getStatesMap(this.entitypatch, layer.animationPlayer.getElapsedTime())); + } + + // put base layer states + if (layer.priority == this.baseLayer.baseLayerPriority) { + stateMap.putAll(this.baseLayer.animationPlayer.getAnimation().get().getStatesMap(this.entitypatch, this.baseLayer.animationPlayer.getElapsedTime())); + } + } + + return new EntityState(stateMap); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/client/Layer.java b/src/main/java/com/tiedup/remake/rig/anim/client/Layer.java new file mode 100644 index 0000000..32af2c4 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/client/Layer.java @@ -0,0 +1,359 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.client; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Supplier; + +import com.google.common.collect.Maps; + +import com.tiedup.remake.rig.anim.AnimationPlayer; +import com.tiedup.remake.rig.anim.LivingMotion; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.types.ConcurrentLinkAnimation; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.types.LayerOffAnimation; +import com.tiedup.remake.rig.anim.types.LinkAnimation; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import yesman.epicfight.gameasset.Animations; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class Layer { + protected AssetAccessor nextAnimation; + protected final LinkAnimation linkAnimation; + protected final ConcurrentLinkAnimation concurrentLinkAnimation; + protected final LayerOffAnimation layerOffAnimation; + protected final Layer.Priority priority; + protected boolean disabled; + protected boolean paused; + public final AnimationPlayer animationPlayer; + + public Layer(Priority priority) { + this(priority, AnimationPlayer::new); + } + + public Layer(Priority priority, Supplier animationPlayerProvider) { + this.animationPlayer = animationPlayerProvider.get(); + this.linkAnimation = new LinkAnimation(); + this.concurrentLinkAnimation = new ConcurrentLinkAnimation(); + this.layerOffAnimation = new LayerOffAnimation(priority); + this.priority = priority; + this.disabled = true; + } + + public void playAnimation(AssetAccessor nextAnimation, LivingEntityPatch entitypatch, float transitionTimeModifier) { + // Get pose before StaticAnimation#end is called + Pose lastPose = this.getCurrentPose(entitypatch); + + if (!this.animationPlayer.isEnd()) { + this.animationPlayer.getAnimation().get().end(entitypatch, nextAnimation, false); + } + + this.resume(); + nextAnimation.get().begin(entitypatch); + + if (!nextAnimation.get().isMetaAnimation()) { + this.setLinkAnimation(nextAnimation, entitypatch, lastPose, transitionTimeModifier); + this.linkAnimation.putOnPlayer(this.animationPlayer, entitypatch); + entitypatch.updateEntityState(); + this.nextAnimation = nextAnimation; + } + } + + /** + * Plays an animation without a link animation + */ + public void playAnimationInstantly(AssetAccessor nextAnimation, LivingEntityPatch entitypatch) { + if (!this.animationPlayer.isEnd()) { + this.animationPlayer.getAnimation().get().end(entitypatch, nextAnimation, false); + } + + this.resume(); + + nextAnimation.get().begin(entitypatch); + nextAnimation.get().putOnPlayer(this.animationPlayer, entitypatch); + entitypatch.updateEntityState(); + this.nextAnimation = null; + } + + protected void playLivingAnimation(AssetAccessor nextAnimation, LivingEntityPatch entitypatch) { + if (!this.animationPlayer.isEnd()) { + this.animationPlayer.getAnimation().get().end(entitypatch, nextAnimation, false); + } + + this.resume(); + nextAnimation.get().begin(entitypatch); + + if (!nextAnimation.get().isMetaAnimation()) { + this.concurrentLinkAnimation.acceptFrom(this.animationPlayer.getRealAnimation(), nextAnimation, this.animationPlayer.getElapsedTime()); + this.concurrentLinkAnimation.putOnPlayer(this.animationPlayer, entitypatch); + entitypatch.updateEntityState(); + this.nextAnimation = nextAnimation; + } + } + + protected Pose getCurrentPose(LivingEntityPatch entitypatch) { + return entitypatch.getClientAnimator().getPose(0.0F, false); + } + + protected void setLinkAnimation(AssetAccessor nextAnimation, LivingEntityPatch entitypatch, Pose lastPose, float transitionTimeModifier) { + AssetAccessor fromAnimation = this.animationPlayer.isEmpty() ? entitypatch.getClientAnimator().baseLayer.animationPlayer.getAnimation() : this.animationPlayer.getAnimation(); + + if (fromAnimation.get() instanceof LinkAnimation linkAnimation) { + fromAnimation = linkAnimation.getFromAnimation(); + } + + nextAnimation.get().setLinkAnimation(fromAnimation, lastPose, !this.animationPlayer.isEmpty(), transitionTimeModifier, entitypatch, this.linkAnimation); + this.linkAnimation.getAnimationClip().setBaked(); + } + + public void update(LivingEntityPatch entitypatch) { + if (this.paused) { + this.animationPlayer.setElapsedTime(this.animationPlayer.getElapsedTime()); + } else { + this.animationPlayer.tick(entitypatch); + } + + if (!this.animationPlayer.isEnd()) { + this.animationPlayer.getAnimation().get().tick(entitypatch); + } else if (!this.paused) { + if (this.nextAnimation != null) { + if (!this.animationPlayer.getAnimation().get().isLinkAnimation() && !this.nextAnimation.get().isLinkAnimation()) { + this.nextAnimation.get().begin(entitypatch); + } + + this.nextAnimation.get().putOnPlayer(this.animationPlayer, entitypatch); + this.nextAnimation = null; + } else { + if (this.animationPlayer.getAnimation() instanceof LayerOffAnimation) { + this.animationPlayer.getAnimation().get().end(entitypatch, Animations.EMPTY_ANIMATION, true); + } else { + this.off(entitypatch); + } + } + } + + if (this.isBaseLayer()) { + entitypatch.updateEntityState(); + entitypatch.updateMotion(true); + } + } + + public void pause() { + this.paused = true; + } + + public void resume() { + this.paused = false; + this.disabled = false; + } + + protected boolean isDisabled() { + return this.disabled; + } + + public boolean isOff() { + return this.isDisabled() || this.animationPlayer.isEmpty(); + } + + protected boolean isBaseLayer() { + return false; + } + + public void copyLayerTo(Layer layer, float playbackTime) { + AssetAccessor animation; + + if (this.animationPlayer.getAnimation() == this.linkAnimation) { + this.linkAnimation.copyTo(layer.linkAnimation); + animation = layer.linkAnimation; + } else { + animation = this.animationPlayer.getAnimation(); + } + + layer.animationPlayer.setPlayAnimation(animation); + layer.animationPlayer.setElapsedTime(this.animationPlayer.getPrevElapsedTime() + playbackTime, this.animationPlayer.getElapsedTime() + playbackTime); + layer.nextAnimation = this.nextAnimation; + layer.resume(); + } + + public LivingMotion getLivingMotion(LivingEntityPatch entitypatch, boolean current) { + return current ? entitypatch.currentLivingMotion : entitypatch.getClientAnimator().currentMotion(); + } + + public Pose getEnabledPose(LivingEntityPatch entitypatch, boolean useCurrentMotion, float partialTick) { + Pose pose = this.animationPlayer.getCurrentPose(entitypatch, partialTick); + this.animationPlayer.getAnimation().get().getJointMaskEntry(entitypatch, useCurrentMotion).ifPresent((jointEntry) -> pose.disableJoint((entry) -> jointEntry.isMasked(this.getLivingMotion(entitypatch, useCurrentMotion), entry.getKey()))); + + return pose; + } + + public void off(LivingEntityPatch entitypatch) { + if (!this.isDisabled() && !(this.animationPlayer.getAnimation() instanceof LayerOffAnimation)) { + if (this.priority == null) { + this.disableLayer(); + } else { + float transitionTimeModifier = entitypatch.getClientAnimator().baseLayer.animationPlayer.getAnimation().get().getTransitionTime(); + setLayerOffAnimation(this.animationPlayer.getAnimation(), this.getEnabledPose(entitypatch, false, 1.0F), this.layerOffAnimation, transitionTimeModifier); + this.playAnimationInstantly(this.layerOffAnimation, entitypatch); + } + } + } + + public void disableLayer() { + this.disabled = true; + this.animationPlayer.setPlayAnimation(Animations.EMPTY_ANIMATION); + } + + public static void setLayerOffAnimation(AssetAccessor currentAnimation, Pose currentPose, LayerOffAnimation offAnimation, float transitionTimeModifier) { + offAnimation.setLastAnimation(currentAnimation.get().getRealAnimation()); + offAnimation.setLastPose(currentPose); + offAnimation.setTotalTime(transitionTimeModifier); + } + + public AssetAccessor getNextAnimation() { + return this.nextAnimation; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append(this.isBaseLayer() ? "Base Layer(" + ((BaseLayer)this).baseLayerPriority + ") : " : " Composite Layer(" + this.priority + ") : "); + sb.append(this.animationPlayer.getAnimation() + " "); + sb.append(", prev elapsed time: " + this.animationPlayer.getPrevElapsedTime() + " "); + sb.append(", elapsed time: " + this.animationPlayer.getElapsedTime() + " "); + sb.append(", total time: " + this.animationPlayer.getAnimation().get().getTotalTime() + " "); + + return sb.toString(); + } + + public static class BaseLayer extends Layer { + protected Map compositeLayers = Maps.newLinkedHashMap(); + protected Layer.Priority baseLayerPriority; + + public BaseLayer() { + this(AnimationPlayer::new); + } + + public BaseLayer(Supplier animationPlayerProvider) { + super(null, animationPlayerProvider); + + for (Priority priority : Priority.values()) { + this.compositeLayers.computeIfAbsent(priority, Layer::new); + } + + this.baseLayerPriority = Priority.LOWEST; + } + + @Override + public void playAnimation(AssetAccessor nextAnimation, LivingEntityPatch entitypatch, float transitionTimeModifier) { + this.offCompositeLayersLowerThan(entitypatch, nextAnimation); + super.playAnimation(nextAnimation, entitypatch, transitionTimeModifier); + this.baseLayerPriority = nextAnimation.get().getPriority(); + } + + @Override + protected void playLivingAnimation(AssetAccessor nextAnimation, LivingEntityPatch entitypatch) { + if (!this.animationPlayer.isEnd()) { + this.animationPlayer.getAnimation().get().end(entitypatch, nextAnimation, false); + } + + this.resume(); + nextAnimation.get().begin(entitypatch); + + if (!nextAnimation.get().isMetaAnimation()) { + this.concurrentLinkAnimation.acceptFrom(this.animationPlayer.getRealAnimation(), nextAnimation, this.animationPlayer.getElapsedTime()); + this.concurrentLinkAnimation.putOnPlayer(this.animationPlayer, entitypatch); + entitypatch.updateEntityState(); + this.nextAnimation = nextAnimation; + } + } + + @Override + public void update(LivingEntityPatch entitypatch) { + super.update(entitypatch); + + for (Layer layer : this.compositeLayers.values()) { + layer.update(entitypatch); + } + } + + public void offCompositeLayersLowerThan(LivingEntityPatch entitypatch, AssetAccessor nextAnimation) { + Priority[] layersToOff = nextAnimation.get().isMainFrameAnimation() ? nextAnimation.get().getPriority().lowersAndEqual() : nextAnimation.get().getPriority().lowers(); + + for (Priority p : layersToOff) { + this.compositeLayers.get(p).off(entitypatch); + } + } + + public void disableLayer(Priority priority) { + this.compositeLayers.get(priority).disableLayer(); + } + + public Layer getLayer(Priority priority) { + return this.compositeLayers.get(priority); + } + + public Priority getBaseLayerPriority() { + return this.baseLayerPriority; + } + + @Override + public void off(LivingEntityPatch entitypatch) { + + } + + @Override + protected boolean isDisabled() { + return false; + } + + @Override + protected boolean isBaseLayer() { + return true; + } + } + + public enum LayerType { + BASE_LAYER, COMPOSITE_LAYER + } + + public enum Priority { + /** + * The common usage of each layer + * + * LOWEST: Most of living cycle animations. Also a default value for animations doesn't inherit {@link MainFrameAnimation.class} + * LOW: A few {@link ActionAnimation.class} that allows showing living cycle animations. e.g. step + * MIDDLE: Most of composite living cycle animations. e.g. weapon holding animations + * HIGH: A few composite animations that doesn't repeat. e.g. Uchigatana sheathing, Shield hit + * HIGHEST: Most of {@link MainFrameAnimation.class} and a few living cycle animations. e.g. ladder animation + **/ + LOWEST, LOW, MIDDLE, HIGH, HIGHEST; + + public Priority[] lowers() { + return Arrays.copyOfRange(Priority.values(), 0, this.ordinal()); + } + + public Priority[] lowersAndEqual() { + return Arrays.copyOfRange(Priority.values(), 0, this.ordinal() + 1); + } + + public Priority[] highers() { + return Arrays.copyOfRange(Priority.values(), this.ordinal(), Priority.values().length); + } + + public boolean isHigherThan(Priority priority) { + return this.ordinal() > priority.ordinal(); + } + + public boolean isHigherOrEqual(Priority priority) { + return this.ordinal() >= priority.ordinal(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/client/property/ClientAnimationProperties.java b/src/main/java/com/tiedup/remake/rig/anim/client/property/ClientAnimationProperties.java new file mode 100644 index 0000000..8f99781 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/client/property/ClientAnimationProperties.java @@ -0,0 +1,51 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.client.property; + +import java.util.List; + +import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty; +import com.tiedup.remake.rig.anim.types.DirectStaticAnimation; +import com.tiedup.remake.rig.anim.client.AnimationSubFileReader; +import com.tiedup.remake.rig.anim.client.Layer; + +public class ClientAnimationProperties { + /** + * Layer type. (BASE: Living, attack animations, COMPOSITE: Aiming, weapon holding, digging animation) + */ + public static final StaticAnimationProperty LAYER_TYPE = new StaticAnimationProperty (); + + /** + * Priority of composite layer. + */ + public static final StaticAnimationProperty PRIORITY = new StaticAnimationProperty (); + + /** + * Joint mask for composite layer. + */ + public static final StaticAnimationProperty JOINT_MASK = new StaticAnimationProperty (); + + /** + * Trail particle information + */ + public static final StaticAnimationProperty> TRAIL_EFFECT = new StaticAnimationProperty> (); + + /** + * An animation clip being played in first person. + */ + public static final StaticAnimationProperty POV_ANIMATION = new StaticAnimationProperty (); + + /** + * An animation clip being played in first person. + */ + public static final StaticAnimationProperty POV_SETTINGS = new StaticAnimationProperty (); + + /** + * Multilayer for living animations (e.g. Greatsword holding animation should be played simultaneously with jumping animation) + */ + public static final StaticAnimationProperty MULTILAYER_ANIMATION = new StaticAnimationProperty (); +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/client/property/JointMask.java b/src/main/java/com/tiedup/remake/rig/anim/client/property/JointMask.java new file mode 100644 index 0000000..ed141db --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/client/property/JointMask.java @@ -0,0 +1,104 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.client.property; + +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.Maps; +import com.mojang.datafixers.util.Pair; + +import com.tiedup.remake.rig.armature.Joint; +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.anim.LivingMotion; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.anim.client.Layer; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class JointMask { + @FunctionalInterface + public interface BindModifier { + public void modify(LivingEntityPatch entitypatch, Pose baseLayerPose, Pose resultPose, LivingMotion livingMotion, JointMaskEntry wholeEntry, Layer.Priority priority, Joint joint, Map, Pose>> poses); + } + + public static final BindModifier KEEP_CHILD_LOCROT = (entitypatch, baseLayerPose, result, livingMotion, wholeEntry, priority, joint, poses) -> { + Pose currentPose = poses.get(priority).getSecond(); + JointTransform lowestTransform = baseLayerPose.orElseEmpty(joint.getName()); + JointTransform currentTransform = currentPose.orElseEmpty(joint.getName()); + result.orElseEmpty(joint.getName()).translation().y = lowestTransform.translation().y; + + OpenMatrix4f lowestMatrix = lowestTransform.toMatrix(); + OpenMatrix4f currentMatrix = currentTransform.toMatrix(); + OpenMatrix4f currentToLowest = OpenMatrix4f.mul(OpenMatrix4f.invert(currentMatrix, null), lowestMatrix, null); + + for (Joint subJoint : joint.getSubJoints()) { + if (wholeEntry.isMasked(livingMotion, subJoint.getName())) { + OpenMatrix4f lowestLocalTransform = OpenMatrix4f.mul(joint.getLocalTransform(), lowestMatrix, null); + OpenMatrix4f currentLocalTransform = OpenMatrix4f.mul(joint.getLocalTransform(), currentMatrix, null); + OpenMatrix4f childTransform = OpenMatrix4f.mul(subJoint.getLocalTransform(), result.orElseEmpty(subJoint.getName()).toMatrix(), null); + OpenMatrix4f lowestFinal = OpenMatrix4f.mul(lowestLocalTransform, childTransform, null); + OpenMatrix4f currentFinal = OpenMatrix4f.mul(currentLocalTransform, childTransform, null); + Vec3f vec = new Vec3f((currentFinal.m30 - lowestFinal.m30) * 0.5F, currentFinal.m31 - lowestFinal.m31, currentFinal.m32 - lowestFinal.m32); + JointTransform jt = result.orElseEmpty(subJoint.getName()); + jt.parent(JointTransform.translation(vec), OpenMatrix4f::mul); + jt.jointLocal(JointTransform.fromMatrixWithoutScale(currentToLowest), OpenMatrix4f::mul); + } + } + }; + + public static JointMask of(String jointName, BindModifier bindModifier) { + return new JointMask(jointName, bindModifier); + } + + public static JointMask of(String jointName) { + return new JointMask(jointName, null); + } + + private final String jointName; + private final BindModifier bindModifier; + + private JointMask(String jointName, BindModifier bindModifier) { + this.jointName = jointName; + this.bindModifier = bindModifier; + } + + public static class JointMaskSet { + final Map masks = Maps.newHashMap(); + + public boolean contains(String name) { + return this.masks.containsKey(name); + } + + public BindModifier getBindModifier(String jointName) { + return this.masks.get(jointName); + } + + public static JointMaskSet of(JointMask... masks) { + JointMaskSet jointMaskSet = new JointMaskSet(); + + for (JointMask jointMask : masks) { + jointMaskSet.masks.put(jointMask.jointName, jointMask.bindModifier); + } + + return jointMaskSet; + } + + public static JointMaskSet of(Set jointMasks) { + JointMaskSet jointMaskSet = new JointMaskSet(); + + for (JointMask jointMask : jointMasks) { + jointMaskSet.masks.put(jointMask.jointName, jointMask.bindModifier); + } + + return jointMaskSet; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/client/property/JointMaskEntry.java b/src/main/java/com/tiedup/remake/rig/anim/client/property/JointMaskEntry.java new file mode 100644 index 0000000..3fa8a22 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/client/property/JointMaskEntry.java @@ -0,0 +1,109 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.client.property; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import net.minecraft.resources.ResourceLocation; +import com.tiedup.remake.rig.anim.LivingMotion; +import com.tiedup.remake.rig.anim.client.property.JointMask.JointMaskSet; + +public class JointMaskEntry { + public static final JointMaskSet BIPED_UPPER_JOINTS_WITH_ROOT = JointMaskSet.of( + JointMask.of("Root", JointMask.KEEP_CHILD_LOCROT), JointMask.of("Torso"), + JointMask.of("Chest"), JointMask.of("Head"), + JointMask.of("Shoulder_R"), JointMask.of("Arm_R"), + JointMask.of("Hand_R"), JointMask.of("Elbow_R"), + JointMask.of("Tool_R"), JointMask.of("Shoulder_L"), + JointMask.of("Arm_L"), JointMask.of("Hand_L"), + JointMask.of("Elbow_L"), JointMask.of("Tool_L") + ); + + public static final JointMaskEntry BASIC_ATTACK_MASK = JointMaskEntry.builder().defaultMask(JointMaskEntry.BIPED_UPPER_JOINTS_WITH_ROOT).create(); + + private final Map masks = Maps.newHashMap(); + private final JointMaskSet defaultMask; + + public JointMaskEntry(JointMaskSet defaultMask, List> masks) { + this.defaultMask = defaultMask; + + for (Pair mask : masks) { + this.masks.put(mask.getLeft(), mask.getRight()); + } + } + + public JointMaskSet getMask(LivingMotion livingmotion) { + return this.masks.getOrDefault(livingmotion, this.defaultMask); + } + + public boolean isMasked(LivingMotion livingmotion, String jointName) { + return !this.masks.getOrDefault(livingmotion, this.defaultMask).contains(jointName); + } + + public Set> getEntries() { + return this.masks.entrySet(); + } + + public JointMaskSet getDefaultMask() { + return this.defaultMask; + } + + public boolean isValid() { + return this.defaultMask != null; + } + + public static JointMaskEntry.Builder builder() { + return new JointMaskEntry.Builder(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + for (Map.Entry entry : this.masks.entrySet()) { + builder.append(entry.getKey() + ": "); + builder.append(JointMaskReloadListener.getKey(entry.getValue()) + ", "); + } + + ResourceLocation maskKey = JointMaskReloadListener.getKey(this.defaultMask); + + if (maskKey == null) { + builder.append("default: custom"); + } else { + builder.append("default: "); + builder.append(JointMaskReloadListener.getKey(this.defaultMask)); + } + + return builder.toString(); + } + + public static class Builder { + private final List> masks = Lists.newArrayList(); + private JointMaskSet defaultMask = null; + + public JointMaskEntry.Builder mask(LivingMotion motion, JointMaskSet masks) { + this.masks.add(Pair.of(motion, masks)); + return this; + } + + public JointMaskEntry.Builder defaultMask(JointMaskSet masks) { + this.defaultMask = masks; + return this; + } + + public JointMaskEntry create() { + return new JointMaskEntry(this.defaultMask, this.masks); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/client/property/JointMaskReloadListener.java b/src/main/java/com/tiedup/remake/rig/anim/client/property/JointMaskReloadListener.java new file mode 100644 index 0000000..4c4d9e0 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/client/property/JointMaskReloadListener.java @@ -0,0 +1,87 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.client.property; + +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import com.tiedup.remake.rig.anim.client.property.JointMask.BindModifier; +import com.tiedup.remake.rig.anim.client.property.JointMask.JointMaskSet; +import yesman.epicfight.main.EpicFightMod; + +public class JointMaskReloadListener extends SimpleJsonResourceReloadListener { + private static final BiMap JOINT_MASKS = HashBiMap.create(); + private static final Map BIND_MODIFIERS = Maps.newHashMap(); + private static final ResourceLocation NONE_MASK = EpicFightMod.identifier("none"); + + static { + BIND_MODIFIERS.put("keep_child_locrot", JointMask.KEEP_CHILD_LOCROT); + } + + public static JointMaskSet getJointMaskEntry(String type) { + ResourceLocation rl = ResourceLocation.parse(type); + return JOINT_MASKS.getOrDefault(rl, JOINT_MASKS.get(NONE_MASK)); + } + + public static JointMaskSet getNoneMask() { + return JOINT_MASKS.get(NONE_MASK); + } + + public static ResourceLocation getKey(JointMaskSet type) { + return JOINT_MASKS.inverse().get(type); + } + + public static Set> entries() { + return JOINT_MASKS.entrySet(); + } + + public JointMaskReloadListener() { + super((new GsonBuilder()).create(), "animmodels/joint_mask"); + } + + @Override + protected void apply(Map objectIn, ResourceManager resourceManager, ProfilerFiller profileFiller) { + JOINT_MASKS.clear(); + + for (Map.Entry entry : objectIn.entrySet()) { + Set masks = Sets.newHashSet(); + JsonObject object = entry.getValue().getAsJsonObject(); + JsonArray joints = object.getAsJsonArray("joints"); + JsonObject bindModifiers = object.has("bind_modifiers") ? object.getAsJsonObject("bind_modifiers") : null; + + for (JsonElement joint : joints) { + String jointName = joint.getAsString(); + BindModifier modifier = null; + + if (bindModifiers != null) { + String modifierName = bindModifiers.has(jointName) ? bindModifiers.get(jointName).getAsString() : null; + modifier = BIND_MODIFIERS.get(modifierName); + } + + masks.add(JointMask.of(jointName, modifier)); + } + + String path = entry.getKey().toString(); + ResourceLocation key = ResourceLocation.fromNamespaceAndPath(entry.getKey().getNamespace(), path.substring(path.lastIndexOf("/") + 1)); + + JOINT_MASKS.put(key, JointMaskSet.of(masks)); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/client/property/LayerInfo.java b/src/main/java/com/tiedup/remake/rig/anim/client/property/LayerInfo.java new file mode 100644 index 0000000..79ee16a --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/client/property/LayerInfo.java @@ -0,0 +1,21 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.client.property; + +import com.tiedup.remake.rig.anim.client.Layer; + +public class LayerInfo { + public final JointMaskEntry jointMaskEntry; + public final Layer.Priority priority; + public final Layer.LayerType layerType; + + public LayerInfo(JointMaskEntry jointMaskEntry, Layer.Priority priority, Layer.LayerType layerType) { + this.jointMaskEntry = jointMaskEntry; + this.priority = priority; + this.layerType = layerType; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/property/AnimationEvent.java b/src/main/java/com/tiedup/remake/rig/anim/property/AnimationEvent.java new file mode 100644 index 0000000..c2841f2 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/property/AnimationEvent.java @@ -0,0 +1,244 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.property; + +import java.util.function.Predicate; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public abstract class AnimationEvent, T extends AnimationEvent> { + protected final AnimationEvent.Side side; + protected final EVENT event; + protected AnimationParameters params; + + private AnimationEvent(AnimationEvent.Side executionSide, EVENT event) { + this.side = executionSide; + this.event = event; + } + + protected abstract boolean checkCondition(LivingEntityPatch entitypatch, AssetAccessor animation, float prevElapsed, float elapsed); + + public void execute(LivingEntityPatch entitypatch, AssetAccessor animation, float prevElapsed, float elapsed) { + if (this.side.predicate.test(entitypatch.getOriginal()) && this.checkCondition(entitypatch, animation, prevElapsed, elapsed)) { + this.event.fire(entitypatch, animation, this.params); + } + } + + public void executeWithNewParams(LivingEntityPatch entitypatch, AssetAccessor animation, float prevElapsed, float elapsed, AnimationParameters parameters) { + if (this.side.predicate.test(entitypatch.getOriginal()) && this.checkCondition(entitypatch, animation, prevElapsed, elapsed)) { + this.event.fire(entitypatch, animation, parameters); + } + } + + public static class SimpleEvent> extends AnimationEvent> { + private SimpleEvent(AnimationEvent.Side executionSide, EVENT event) { + super(executionSide, event); + } + + @Override + protected boolean checkCondition(LivingEntityPatch entitypatch, AssetAccessor animation, float prevElapsed, float elapsed) { + return true; + } + + public static > SimpleEvent create(E event, AnimationEvent.Side isRemote) { + return new SimpleEvent<> (isRemote, event); + } + } + + public static class InTimeEvent> extends AnimationEvent> implements Comparable> { + final float time; + + private InTimeEvent(float time, AnimationEvent.Side executionSide, EVENT event) { + super(executionSide, event); + this.time = time; + } + + @Override + public boolean checkCondition(LivingEntityPatch entitypatch, AssetAccessor animation, float prevElapsed, float elapsed) { + return this.time >= prevElapsed && this.time < elapsed; + } + + @Override + public int compareTo(InTimeEvent arg0) { + if(this.time == arg0.time) { + return 0; + } else { + return this.time > arg0.time ? 1 : -1; + } + } + + public static > InTimeEvent create(float time, E event, AnimationEvent.Side isRemote) { + return new InTimeEvent<> (time, isRemote, event); + } + } + + public static class InPeriodEvent> extends AnimationEvent> implements Comparable> { + final float start; + final float end; + + private InPeriodEvent(float start, float end, AnimationEvent.Side executionSide, EVENT event) { + super(executionSide, event); + this.start = start; + this.end = end; + } + + @Override + public boolean checkCondition(LivingEntityPatch entitypatch, AssetAccessor animation, float prevElapsed, float elapsed) { + return this.start <= elapsed && this.end > elapsed; + } + + @Override + public int compareTo(InPeriodEvent arg0) { + if (this.start == arg0.start) { + return 0; + } else { + return this.start > arg0.start ? 1 : -1; + } + } + + public static > InPeriodEvent create(float start, float end, E event, AnimationEvent.Side isRemote) { + return new InPeriodEvent<> (start, end, isRemote, event); + } + } + + public enum Side { + CLIENT((entity) -> entity.level().isClientSide), + SERVER((entity) -> !entity.level().isClientSide), BOTH((entity) -> true), + LOCAL_CLIENT((entity) -> { + if (entity instanceof Player player) { + return player.isLocalPlayer(); + } + + return false; + }); + + Predicate predicate; + + Side(Predicate predicate) { + this.predicate = predicate; + } + } + + public AnimationParameters getParameters() { + return this.params; + } + + public T params(A p1) { + this.params = AnimationParameters.of(p1); + return (T)this; + } + + public T params(A p1, B p2) { + this.params = AnimationParameters.of(p1, p2); + return (T)this; + } + + public T params(A p1, B p2, C p3) { + this.params = AnimationParameters.of(p1, p2, p3); + return (T)this; + } + + public T params(A p1, B p2, C p3, D p4) { + this.params = AnimationParameters.of(p1, p2, p3, p4); + return (T)this; + } + + public T params(A p1, B p2, C p3, D p4, E p5) { + this.params = AnimationParameters.of(p1, p2, p3, p4, p5); + return (T)this; + } + + public T params(A p1, B p2, C p3, D p4, E p5, F p6) { + this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6); + return (T)this; + } + + public T params(A p1, B p2, C p3, D p4, E p5, F p6, G p7) { + this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6, p7); + return (T)this; + } + + public T params(A p1, B p2, C p3, D p4, E p5, F p6, G p7, H p8) { + this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6, p7, p8); + return (T)this; + } + + public T params(A p1, B p2, C p3, D p4, E p5, F p6, G p7, H p8, I p9) { + this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6, p7, p8, p9); + return (T)this; + } + + public T params(A p1, B p2, C p3, D p4, E p5, F p6, G p7, H p8, I p9, J p10) { + this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); + return (T)this; + } + + @FunctionalInterface + public interface Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E0 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E1 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E2 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E3 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E4 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E5 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E6 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E7 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E8 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E9 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } + + @FunctionalInterface + public interface E10 extends Event { + void fire(LivingEntityPatch entitypatch, AssetAccessor animation, AnimationParameters params); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/property/AnimationParameters.java b/src/main/java/com/tiedup/remake/rig/anim/property/AnimationParameters.java new file mode 100644 index 0000000..99a4843 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/property/AnimationParameters.java @@ -0,0 +1,86 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.property; + +public record AnimationParameters ( + A first, + B second, + C third, + D fourth, + E fifth, + F sixth, + G seventh, + H eighth, + I ninth, + J tenth +) { + public static AnimationParameters of(A first) { + return new AnimationParameters<> (first, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null); + } + + public static AnimationParameters of(A first, B second) { + return new AnimationParameters<> (first, second, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null); + } + + public static AnimationParameters of(A first, B second, C third) { + return new AnimationParameters<> (first, second, third, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null); + } + + public static AnimationParameters of(A first, B second, C third, D fourth) { + return new AnimationParameters<> (first, second, third, fourth, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null); + } + + public static AnimationParameters of(A first, B second, C third, D fourth, E fifth) { + return new AnimationParameters<> (first, second, third, fourth, fifth, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null); + } + + public static AnimationParameters of(A first, B second, C third, D fourth, E fifth, F sixth) { + return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, (Void)null, (Void)null, (Void)null, (Void)null); + } + + public static AnimationParameters of(A first, B second, C third, D fourth, E fifth, F sixth, G seventh) { + return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, seventh, (Void)null, (Void)null, (Void)null); + } + + public static AnimationParameters of(A first, B second, C third, D fourth, E fifth, F sixth, G seventh, H eighth) { + return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, seventh, eighth, (Void)null, (Void)null); + } + + public static AnimationParameters of(A first, B second, C third, D fourth, E fifth, F sixth, G seventh, H eighth, I ninth) { + return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, seventh, eighth, ninth, (Void)null); + } + + public static AnimationParameters of(A first, B second, C third, D fourth, E fifth, F sixth, G seventh, H eighth, I ninth, J tenth) { + return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, seventh, eighth, ninth, tenth); + } + + public static AnimationParameters addParameter(AnimationParameters parameters, N newParam) { + if (parameters.first() == null) { + return new AnimationParameters (newParam, null, null, null, null, null, null, null, null, null); + } else if (parameters.second() == null) { + return new AnimationParameters (parameters.first(), newParam, null, null, null, null, null, null, null, null); + } else if (parameters.third() == null) { + return new AnimationParameters (parameters.first(), parameters.second(), newParam, null, null, null, null, null, null, null); + } else if (parameters.fourth() == null) { + return new AnimationParameters (parameters.first(), parameters.second(), parameters.third(), newParam, null, null, null, null, null, null); + } else if (parameters.fifth() == null) { + return new AnimationParameters (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), newParam, null, null, null, null, null); + } else if (parameters.sixth() == null) { + return new AnimationParameters (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), newParam, null, null, null, null); + } else if (parameters.seventh() == null) { + return new AnimationParameters (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), parameters.sixth(), newParam, null, null, null); + } else if (parameters.eighth() == null) { + return new AnimationParameters (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), parameters.sixth(), parameters.seventh(), newParam, null, null); + } else if (parameters.ninth() == null) { + return new AnimationParameters (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), parameters.sixth(), parameters.seventh(), parameters.eighth(), newParam, null); + } else if (parameters.tenth() == null) { + return new AnimationParameters (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), parameters.sixth(), parameters.seventh(), parameters.eighth(), parameters.ninth(), newParam); + } + + throw new UnsupportedOperationException("Parameters are full!"); + } +} diff --git a/src/main/java/com/tiedup/remake/rig/anim/property/AnimationProperty.java b/src/main/java/com/tiedup/remake/rig/anim/property/AnimationProperty.java new file mode 100644 index 0000000..cd9e07c --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/property/AnimationProperty.java @@ -0,0 +1,381 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.property; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import javax.annotation.Nullable; + +import com.google.common.collect.Maps; +import com.google.gson.JsonElement; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.tags.TagKey; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.registries.RegistryObject; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.LivingMotion; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.TransformSheet; +import com.tiedup.remake.rig.anim.property.AnimationEvent.SimpleEvent; +import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordGetter; +import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordSetter; +import com.tiedup.remake.rig.anim.types.ActionAnimation; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.types.LinkAnimation; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import yesman.epicfight.api.physics.ik.InverseKinematicsSimulator.BakedInverseKinematicsDefinition; +import yesman.epicfight.api.physics.ik.InverseKinematicsSimulator.InverseKinematicsDefinition; +import com.tiedup.remake.rig.util.HitEntityList.Priority; +import com.tiedup.remake.rig.util.TimePairList; +import com.tiedup.remake.rig.math.ValueModifier; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.particle.HitParticleType; +import yesman.epicfight.skill.BasicAttack; +import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.patch.item.CapabilityItem; +import yesman.epicfight.world.damagesource.ExtraDamageInstance; +import yesman.epicfight.world.damagesource.StunType; + +public abstract class AnimationProperty { + private static final Map> SERIALIZABLE_ANIMATION_PROPERTY_KEYS = Maps.newHashMap(); + + @SuppressWarnings("unchecked") + public static AnimationProperty getSerializableProperty(String name) { + if (!SERIALIZABLE_ANIMATION_PROPERTY_KEYS.containsKey(name)) { + throw new IllegalStateException("No property key named " + name); + } + + return (AnimationProperty) SERIALIZABLE_ANIMATION_PROPERTY_KEYS.get(name); + } + + private final Codec codecs; + private final String name; + + public AnimationProperty(String name, @Nullable Codec codecs) { + this.codecs = codecs; + this.name = name; + + if (name != null) { + if (SERIALIZABLE_ANIMATION_PROPERTY_KEYS.containsKey(name)) { + throw new IllegalStateException("Animation property key " + name + " is already registered."); + } + + SERIALIZABLE_ANIMATION_PROPERTY_KEYS.put(name, this); + } + } + + public AnimationProperty(String name) { + this(name, null); + } + + public T parseFrom(JsonElement e) { + return this.codecs.parse(JsonOps.INSTANCE, e).resultOrPartial((errm) -> EpicFightMod.LOGGER.warn("Failed to parse property " + this.name + " because of " + errm)).orElseThrow(); + } + + public Codec getCodecs() { + return this.codecs; + } + + public static class StaticAnimationProperty extends AnimationProperty { + public StaticAnimationProperty(String rl, @Nullable Codec codecs) { + super(rl, codecs); + } + + public StaticAnimationProperty() { + this(null, null); + } + + /** + * Events that are fired in every tick. + */ + public static final StaticAnimationProperty>> TICK_EVENTS = new StaticAnimationProperty>> (); + + /** + * Events that are fired when the animation starts. + */ + public static final StaticAnimationProperty>> ON_BEGIN_EVENTS = new StaticAnimationProperty>> (); + + /** + * Events that are fired when the animation ends. + */ + public static final StaticAnimationProperty>> ON_END_EVENTS = new StaticAnimationProperty>> (); + + /** + * An event triggered when entity changes an item in hand. + */ + public static final StaticAnimationProperty>> ON_ITEM_CHANGE_EVENT = new StaticAnimationProperty>> (); + + /** + * You can modify the playback speed of the animation. + */ + public static final StaticAnimationProperty PLAY_SPEED_MODIFIER = new StaticAnimationProperty (); + + /** + * You can modify the playback speed of the animation. + */ + public static final StaticAnimationProperty ELAPSED_TIME_MODIFIER = new StaticAnimationProperty (); + + /** + * This property will be called both in client and server when modifying the pose + */ + public static final StaticAnimationProperty POSE_MODIFIER = new StaticAnimationProperty (); + + /** + * Fix the head rotation to the player's body rotation + */ + public static final StaticAnimationProperty FIXED_HEAD_ROTATION = new StaticAnimationProperty (); + + /** + * Defines static animations as link animation when the animation is followed by a specific animation + */ + public static final StaticAnimationProperty>> TRANSITION_ANIMATIONS_FROM = new StaticAnimationProperty>> (); + + /** + * Defines static animations as link animation when the animation is following a specific animation + */ + public static final StaticAnimationProperty>> TRANSITION_ANIMATIONS_TO = new StaticAnimationProperty>> (); + + /** + * Disable physics while playing animation + */ + public static final StaticAnimationProperty NO_PHYSICS = new StaticAnimationProperty ("no_physics", Codec.BOOL); + + /** + * Inverse kinematics information + */ + public static final StaticAnimationProperty> IK_DEFINITION = new StaticAnimationProperty> (); + + /** + * This property automatically baked when animation is loaded + */ + public static final StaticAnimationProperty> BAKED_IK_DEFINITION = new StaticAnimationProperty> (); + + /** + * This property reset the entity's living motion + */ + public static final StaticAnimationProperty RESET_LIVING_MOTION = new StaticAnimationProperty (); + } + + public static class ActionAnimationProperty extends StaticAnimationProperty { + public ActionAnimationProperty(String rl, @Nullable Codec codecs) { + super(rl, codecs); + } + + public ActionAnimationProperty() { + this(null, null); + } + + /** + * This property will set the entity's delta movement to (0, 0, 0) at the beginning of an animation if true. + */ + public static final ActionAnimationProperty STOP_MOVEMENT = new ActionAnimationProperty ("stop_movements", Codec.BOOL); + + /** + * This property will set the entity's delta movement to (0, 0, 0) at the beginning of an animation if true. + */ + public static final ActionAnimationProperty REMOVE_DELTA_MOVEMENT = new ActionAnimationProperty ("revmoe_delta_move", Codec.BOOL); + + /** + * This property will move entity's coord also as y axis if true. + * Don't recommend using this property because it's old system. Use the coord joint instead. + */ + public static final ActionAnimationProperty MOVE_VERTICAL = new ActionAnimationProperty ("move_vertically", Codec.BOOL); + + /** + * This property determines the time of entity not affected by gravity. + */ + public static final ActionAnimationProperty NO_GRAVITY_TIME = new ActionAnimationProperty (); + + /** + * Coord of action animation + */ + public static final ActionAnimationProperty COORD = new ActionAnimationProperty (); + + /** + * This property determines whether to move the entity in link animation or not. + */ + public static final ActionAnimationProperty MOVE_ON_LINK = new ActionAnimationProperty ("move_during_link", Codec.BOOL); + + /** + * You can specify the coord movement time in action animation. Must be registered in order of time. + */ + public static final ActionAnimationProperty MOVE_TIME = new ActionAnimationProperty (); + + /** + * Set the dynamic coordinates of {@link ActionAnimation}. Called before creation of {@link LinkAnimation}. + */ + public static final ActionAnimationProperty COORD_SET_BEGIN = new ActionAnimationProperty (); + + /** + * Set the dynamic coordinates of {@link ActionAnimation}. + */ + public static final ActionAnimationProperty COORD_SET_TICK = new ActionAnimationProperty (); + + /** + * Set the coordinates of action animation. + */ + public static final ActionAnimationProperty COORD_GET = new ActionAnimationProperty (); + + /** + * This property determines if the speed effect will increase the move distance. + */ + public static final ActionAnimationProperty AFFECT_SPEED = new ActionAnimationProperty ("move_speed_based_distance", Codec.BOOL); + + /** + * This property determines if the movement can be canceled by {@link LivingEntityPatch#shouldBlockMoving()}. + */ + public static final ActionAnimationProperty CANCELABLE_MOVE = new ActionAnimationProperty ("cancellable_movement", Codec.BOOL); + + /** + * Death animations won't be played if this value is true + */ + public static final ActionAnimationProperty IS_DEATH_ANIMATION = new ActionAnimationProperty ("is_death", Codec.BOOL); + + /** + * This property determines the update time of {@link ActionAnimationProperty#COORD_SET_TICK}. If the current time out of the bound it uses {@link MoveCoordFunctions#RAW_COORD and MoveCoordFunctions#DIFF_FROM_PREV_COORD}} + */ + public static final ActionAnimationProperty COORD_UPDATE_TIME = new ActionAnimationProperty (); + + /** + * This property determines if it reset the player basic attack combo counter or not {@link BasicAttack} + */ + public static final ActionAnimationProperty RESET_PLAYER_COMBO_COUNTER = new ActionAnimationProperty ("reset_combo_attack_counter", Codec.BOOL); + + /** + * Provide destination of action animation {@link MoveCoordFunctions} + */ + public static final ActionAnimationProperty DEST_LOCATION_PROVIDER = new ActionAnimationProperty (); + + /** + * Provide y rotation of entity {@link MoveCoordFunctions} + */ + public static final ActionAnimationProperty ENTITY_YROT_PROVIDER = new ActionAnimationProperty (); + + /** + * Provide y rotation of tracing coord {@link MoveCoordFunctions} + */ + public static final ActionAnimationProperty DEST_COORD_YROT_PROVIDER = new ActionAnimationProperty (); + + /** + * Decides the index of start key frame for coord transform, See also with {@link MoveCoordFunctions#TRACE_ORIGIN_AS_DESTINATION} + */ + public static final ActionAnimationProperty COORD_START_KEYFRAME_INDEX = new ActionAnimationProperty (); + + /** + * Decides the index of destination key frame for coord transform, See also with {@link MoveCoordFunctions#TRACE_ORIGIN_AS_DESTINATION} + */ + public static final ActionAnimationProperty COORD_DEST_KEYFRAME_INDEX = new ActionAnimationProperty (); + + /** + * Determines if an entity should look where a camera is looking at the beginning of an animation (player only) + */ + public static final ActionAnimationProperty SYNC_CAMERA = new ActionAnimationProperty ("sync_camera", Codec.BOOL); + } + + public static class AttackAnimationProperty extends ActionAnimationProperty { + public AttackAnimationProperty(String rl, @Nullable Codec codecs) { + super(rl, codecs); + } + + public AttackAnimationProperty() { + this(null, null); + } + + /** + * This property determines if the animation has a fixed amount of move distance not depending on the distance between attacker and target entity + */ + public static final AttackAnimationProperty FIXED_MOVE_DISTANCE = new AttackAnimationProperty ("fixed_movement_distance", Codec.BOOL); + + /** + * This property determines how much the playback speed will be affected by entity's attack speed. + */ + public static final AttackAnimationProperty ATTACK_SPEED_FACTOR = new AttackAnimationProperty ("attack_speed_factor", Codec.FLOAT); + + /** + * This property determines the basis of the speed factor. Default basis is the total animation time. + */ + public static final AttackAnimationProperty BASIS_ATTACK_SPEED = new AttackAnimationProperty ("basis_attack_speed", Codec.FLOAT); + + /** + * This property adds interpolated colliders when detecting colliding entities by using @MultiCollider. + */ + public static final AttackAnimationProperty EXTRA_COLLIDERS = new AttackAnimationProperty ("extra_colliders", Codec.INT); + + /** + * This property determines a minimal distance between attacker and target. + */ + public static final AttackAnimationProperty REACH = new AttackAnimationProperty ("reach", Codec.FLOAT); + } + + public static class AttackPhaseProperty { + public AttackPhaseProperty(String rl, @Nullable Codec codecs) { + //super(rl, codecs); + } + + public AttackPhaseProperty() { + //this(null, null); + } + + public static final AttackPhaseProperty MAX_STRIKES_MODIFIER = new AttackPhaseProperty ("max_strikes", ValueModifier.CODEC); + public static final AttackPhaseProperty DAMAGE_MODIFIER = new AttackPhaseProperty ("damage", ValueModifier.CODEC); + public static final AttackPhaseProperty ARMOR_NEGATION_MODIFIER = new AttackPhaseProperty ("armor_negation", ValueModifier.CODEC); + public static final AttackPhaseProperty IMPACT_MODIFIER = new AttackPhaseProperty ("impact", ValueModifier.CODEC); + public static final AttackPhaseProperty> EXTRA_DAMAGE = new AttackPhaseProperty> (); + public static final AttackPhaseProperty STUN_TYPE = new AttackPhaseProperty (); + public static final AttackPhaseProperty SWING_SOUND = new AttackPhaseProperty (); + public static final AttackPhaseProperty HIT_SOUND = new AttackPhaseProperty (); + public static final AttackPhaseProperty> PARTICLE = new AttackPhaseProperty> (); + public static final AttackPhaseProperty HIT_PRIORITY = new AttackPhaseProperty (); + public static final AttackPhaseProperty>> SOURCE_TAG = new AttackPhaseProperty>> (); + public static final AttackPhaseProperty, Vec3>> SOURCE_LOCATION_PROVIDER = new AttackPhaseProperty, Vec3>> (); + } + + @FunctionalInterface + public interface Registerer { + void register(Map, Object> properties, AnimationProperty key, T object); + } + + /****************************** + * Static Animation Properties + ******************************/ + /** + * elapsedTime contains partial tick + */ + @FunctionalInterface + public interface PoseModifier { + void modify(DynamicAnimation self, Pose pose, LivingEntityPatch entitypatch, float elapsedTime, float partialTick); + } + + @FunctionalInterface + public interface PlaybackSpeedModifier { + float modify(DynamicAnimation self, LivingEntityPatch entitypatch, float speed, float prevElapsedTime, float elapsedTime); + } + + @FunctionalInterface + public interface PlaybackTimeModifier { + Pair modify(DynamicAnimation self, LivingEntityPatch entitypatch, float speed, float prevElapsedTime, float elapsedTime); + } + + @FunctionalInterface + public interface DestLocationProvider { + Vec3 get(DynamicAnimation self, LivingEntityPatch entitypatch); + } + + @FunctionalInterface + public interface YRotProvider { + float get(DynamicAnimation self, LivingEntityPatch entitypatch); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/property/MoveCoordFunctions.java b/src/main/java/com/tiedup/remake/rig/anim/property/MoveCoordFunctions.java new file mode 100644 index 0000000..8ffd9c6 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/property/MoveCoordFunctions.java @@ -0,0 +1,475 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.property; + +import java.util.Optional; + +import net.minecraft.core.BlockPos; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import com.tiedup.remake.rig.anim.AnimationPlayer; +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.anim.Keyframe; +import com.tiedup.remake.rig.anim.SynchedAnimationVariableKeys; +import com.tiedup.remake.rig.anim.TransformSheet; +import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty; +import com.tiedup.remake.rig.anim.property.AnimationProperty.AttackAnimationProperty; +import com.tiedup.remake.rig.anim.property.AnimationProperty.DestLocationProvider; +import com.tiedup.remake.rig.anim.property.AnimationProperty.YRotProvider; +import com.tiedup.remake.rig.anim.types.ActionAnimation; +import com.tiedup.remake.rig.anim.types.AttackAnimation; +import com.tiedup.remake.rig.anim.types.AttackAnimation.Phase; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.types.EntityState; +import com.tiedup.remake.rig.anim.types.grappling.GrapplingAttackAnimation; +import com.tiedup.remake.rig.math.MathUtils; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; +import com.tiedup.remake.rig.math.Vec4f; +import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.patch.MobPatch; + +public class MoveCoordFunctions { + /** + * Defines a function that how to interpret given coordinate and return the movement vector from entity's current position + */ + @FunctionalInterface + public interface MoveCoordGetter { + Vec3f get(DynamicAnimation animation, LivingEntityPatch entitypatch, TransformSheet transformSheet, float prevElapsedTime, float elapsedTime); + } + + /** + * Defines a function that how to build the coordinate of {@link ActionAnimation} + */ + @FunctionalInterface + public interface MoveCoordSetter { + void set(DynamicAnimation animation, LivingEntityPatch entitypatch, TransformSheet transformSheet); + } + + /** + * MODEL_COORD + * - Calculates the coordinate gap between previous and current elapsed time + * - the coordinate doesn't reflect the entity's rotation + */ + public static final MoveCoordGetter MODEL_COORD = (animation, entitypatch, coord, prevElapsedTime, elapsedTime) -> { + LivingEntity livingentity = entitypatch.getOriginal(); + JointTransform oJt = coord.getInterpolatedTransform(prevElapsedTime); + JointTransform jt = coord.getInterpolatedTransform(elapsedTime); + Vec4f prevpos = new Vec4f(oJt.translation()); + Vec4f currentpos = new Vec4f(jt.translation()); + + OpenMatrix4f rotationTransform = entitypatch.getModelMatrix(1.0F).removeTranslation().removeScale(); + OpenMatrix4f localTransform = entitypatch.getArmature().searchJointByName("Root").getLocalTransform().removeTranslation(); + rotationTransform.mulBack(localTransform); + currentpos.transform(rotationTransform); + prevpos.transform(rotationTransform); + + boolean hasNoGravity = entitypatch.getOriginal().isNoGravity(); + boolean moveVertical = animation.getProperty(ActionAnimationProperty.MOVE_VERTICAL).orElse(false) || animation.getProperty(ActionAnimationProperty.COORD).isPresent(); + float dx = prevpos.x - currentpos.x; + float dy = (moveVertical || hasNoGravity) ? currentpos.y - prevpos.y : 0.0F; + float dz = prevpos.z - currentpos.z; + dx = Math.abs(dx) > 0.0001F ? dx : 0.0F; + dz = Math.abs(dz) > 0.0001F ? dz : 0.0F; + + BlockPos blockpos = new BlockPos.MutableBlockPos(livingentity.getX(), livingentity.getBoundingBox().minY - 1.0D, livingentity.getZ()); + BlockState blockState = livingentity.level().getBlockState(blockpos); + AttributeInstance movementSpeed = livingentity.getAttribute(Attributes.MOVEMENT_SPEED); + boolean soulboost = blockState.is(BlockTags.SOUL_SPEED_BLOCKS) && EnchantmentHelper.getEnchantmentLevel(Enchantments.SOUL_SPEED, livingentity) > 0; + float speedFactor = (float)(soulboost ? 1.0D : livingentity.level().getBlockState(blockpos).getBlock().getSpeedFactor()); + float moveMultiplier = (float)(animation.getProperty(ActionAnimationProperty.AFFECT_SPEED).orElse(false) ? (movementSpeed.getValue() / movementSpeed.getBaseValue()) : 1.0F); + + return new Vec3f(dx * moveMultiplier * speedFactor, dy, dz * moveMultiplier * speedFactor); + }; + + /** + * WORLD_COORD + * - Calculates the coordinate of current elapsed time + * - the coordinate is the world position + */ + public static final MoveCoordGetter WORLD_COORD = (animation, entitypatch, coord, prevElapsedTime, elapsedTime) -> { + JointTransform jt = coord.getInterpolatedTransform(elapsedTime); + Vec3 entityPos = entitypatch.getOriginal().position(); + + return jt.translation().copy().sub(Vec3f.fromDoubleVector(entityPos)); + }; + + /** + * ATTACHED + * Calculates the relative position of a grappling target entity. + * - especially used by {@link GrapplingAttackAnimation} + * - read by {@link MoveCoordFunctions#RAW_COORD} + */ + public static final MoveCoordGetter ATTACHED = (animation, entitypatch, coord, prevElapsedTime, elapsedTime) -> { + LivingEntity target = entitypatch.getGrapplingTarget(); + + if (target == null) { + return MODEL_COORD.get(animation, entitypatch, coord, prevElapsedTime, elapsedTime); + } + + TransformSheet rootCoord = animation.getCoord(); + LivingEntity livingentity = entitypatch.getOriginal(); + Vec3f model = rootCoord.getInterpolatedTransform(elapsedTime).translation(); + Vec3f world = OpenMatrix4f.transform3v(OpenMatrix4f.createRotatorDeg(-target.getYRot(), Vec3f.Y_AXIS), model, null); + Vec3f dst = Vec3f.fromDoubleVector(target.position()).add(world); + entitypatch.setYRot(Mth.wrapDegrees(target.getYRot() + 180.0F)); + + return dst.sub(Vec3f.fromDoubleVector(livingentity.position())); + }; + + /****************************************************** + * Action animation properties + ******************************************************/ + + /** + * No destination + */ + public static final DestLocationProvider NO_DEST = (DynamicAnimation self, LivingEntityPatch entitypatch) -> { + return null; + }; + + /** + * Location of the current attack target + */ + public static final DestLocationProvider ATTACK_TARGET_LOCATION = (DynamicAnimation self, LivingEntityPatch entitypatch) -> { + return entitypatch.getTarget() == null ? null : entitypatch.getTarget().position(); + }; + + /** + * Location set by Animation Variable + */ + public static final DestLocationProvider SYNCHED_DEST_VARIABLE = (DynamicAnimation self, LivingEntityPatch entitypatch) -> { + return entitypatch.getAnimator().getVariables().getOrDefault(SynchedAnimationVariableKeys.DESTINATION.get(), self.getRealAnimation()); + }; + + /** + * Location of current attack target that is provided by animation variable + */ + public static final DestLocationProvider SYNCHED_TARGET_ENTITY_LOCATION_VARIABLE = (DynamicAnimation self, LivingEntityPatch entitypatch) -> { + Optional targetEntityId = entitypatch.getAnimator().getVariables().get(SynchedAnimationVariableKeys.TARGET_ENTITY.get(), self.getRealAnimation()); + + if (targetEntityId.isPresent()) { + Entity entity = entitypatch.getOriginal().level().getEntity(targetEntityId.get()); + + if (entity != null) { + return entity.position(); + } + } + + return entitypatch.getOriginal().position(); + }; + + /** + * Looking direction from an action beginning location to a destination location + */ + public static final YRotProvider LOOK_DEST = (DynamicAnimation self, LivingEntityPatch entitypatch) -> { + Vec3 destLocation = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER).orElse(NO_DEST).get(self, entitypatch); + + if (destLocation != null) { + Vec3 startInWorld = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.BEGINNING_LOCATION, self.getRealAnimation()); + + if (startInWorld == null) { + startInWorld = entitypatch.getOriginal().position(); + } + + Vec3 toDestWorld = destLocation.subtract(startInWorld); + float yRot = (float)Mth.wrapDegrees(MathUtils.getYRotOfVector(toDestWorld)); + float entityYRot = MathUtils.rotlerp(entitypatch.getYRot(), yRot, entitypatch.getYRotLimit()); + + return entityYRot; + } else { + return entitypatch.getYRot(); + } + }; + + /** + * Rotate an entity toward target for attack animations + */ + public static final YRotProvider MOB_ATTACK_TARGET_LOOK = (DynamicAnimation self, LivingEntityPatch entitypatch) -> { + if (!entitypatch.isLogicalClient() && entitypatch instanceof MobPatch mobpatch) { + AnimationPlayer player = entitypatch.getAnimator().getPlayerFor(self.getAccessor()); + float elapsedTime = player.getElapsedTime(); + EntityState state = self.getState(entitypatch, elapsedTime); + + if (state.getLevel() == 1 && !state.turningLocked()) { + mobpatch.getOriginal().getNavigation().stop(); + entitypatch.getOriginal().attackAnim = 2; + LivingEntity target = entitypatch.getTarget(); + + if (target != null) { + float currentYRot = Mth.wrapDegrees(entitypatch.getOriginal().getYRot()); + float clampedYRot = entitypatch.getYRotDeltaTo(target); + + return currentYRot + clampedYRot; + } + } + } + + return entitypatch.getYRot(); + }; + + /****************************************************** + * MoveCoordSetters + * Consider that getAnimationPlayer(self) returns null at the beginning. + ******************************************************/ + /** + * Sets a raw animation coordinate as action animation's coord + * - read by {@link MoveCoordFunctions#MODEL_COORD} + */ + public static final MoveCoordSetter RAW_COORD = (self, entitypatch, transformSheet) -> { + transformSheet.readFrom(self.getCoord().copyAll()); + }; + + /** + * Sets a raw animation coordinate multiplied by entity's pitch as action animation's coord + * - read by {@link MoveCoordFunctions#MODEL_COORD} + */ + public static final MoveCoordSetter RAW_COORD_WITH_X_ROT = (self, entitypatch, transformSheet) -> { + TransformSheet sheet = self.getCoord().copyAll(); + float xRot = entitypatch.getOriginal().getXRot(); + + for (Keyframe kf : sheet.getKeyframes()) { + kf.transform().translation().rotate(-xRot, Vec3f.X_AXIS); + } + + transformSheet.readFrom(sheet); + }; + + /** + * Trace the origin point(0, 0, 0) in blender coord system as the destination + * - specify the {@link ActionAnimationProperty#DEST_LOCATION_PROVIDER} or it will act as {@link MoveCoordFunctions#RAW_COORD}. + * - the first keyframe's location is where the entity is in world + * - you can specify target frame distance by {@link ActionAnimationProperty#COORD_START_KEYFRAME_INDEX}, {@link ActionAnimationProperty#COORD_DEST_KEYFRAME_INDEX} + * - the coord after destination frame will not be scaled or rotated by distance gap between start location and end location in world coord + * - entity's x rotation is not affected by this coord function + * - entity's y rotation is the direction toward a destination, or you can give specific rotation value by {@link ActionAnimation#ENTITY_Y_ROT AnimationProperty} + * - no movements in link animation + * - read by {@link MoveCoordFunctions#WORLD_COORD} + */ + public static final MoveCoordSetter TRACE_ORIGIN_AS_DESTINATION = (self, entitypatch, transformSheet) -> { + if (self.isLinkAnimation()) { + transformSheet.readFrom(TransformSheet.EMPTY_SHEET_PROVIDER.apply(entitypatch.getOriginal().position())); + return; + } + + Keyframe[] coordKeyframes = self.getCoord().getKeyframes(); + int startFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_START_KEYFRAME_INDEX).orElse(0); + int destFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX).orElse(coordKeyframes.length - 1); + Vec3 destInWorld = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER).orElse(NO_DEST).get(self, entitypatch); + + if (destInWorld == null) { + Vec3f beginningPosition = coordKeyframes[0].transform().translation().copy().multiply(1.0F, 1.0F, -1.0F); + beginningPosition.rotate(-entitypatch.getYRot(), Vec3f.Y_AXIS); + destInWorld = entitypatch.getOriginal().position().add(-beginningPosition.x, -beginningPosition.y, -beginningPosition.z); + } + + Vec3 startInWorld = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.BEGINNING_LOCATION, self.getRealAnimation()); + + if (startInWorld == null) { + startInWorld = entitypatch.getOriginal().position(); + } + + Vec3 toTargetInWorld = destInWorld.subtract(startInWorld); + float yRot = (float)Mth.wrapDegrees(MathUtils.getYRotOfVector(toTargetInWorld)); + Optional destYRotProvider = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_COORD_YROT_PROVIDER); + float destYRot = destYRotProvider.isEmpty() ? yRot : destYRotProvider.get().get(self, entitypatch); + + TransformSheet result = self.getCoord().transformToWorldCoordOriginAsDest(entitypatch, startInWorld, destInWorld, yRot, destYRot, startFrame, destFrame); + transformSheet.readFrom(result); + }; + + /** + * Trace the target entity's position (use it with MODEL_COORD) + * - the location of the last keyfram is basis to limit maximum distance + * - rotation is where the entity is looking + */ + public static final MoveCoordSetter TRACE_TARGET_DISTANCE = (self, entitypatch, transformSheet) -> { + Vec3 destLocation = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER).orElse(NO_DEST).get(self, entitypatch); + + if (destLocation != null) { + TransformSheet transform = self.getCoord().copyAll(); + Keyframe[] coord = transform.getKeyframes(); + Keyframe[] realAnimationCoord = self.getRealAnimation().get().getCoord().getKeyframes(); + Vec3 startInWorld = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.BEGINNING_LOCATION, self.getRealAnimation()); + + if (startInWorld == null) { + startInWorld = entitypatch.getOriginal().position(); + } + + int startFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_START_KEYFRAME_INDEX).orElse(0); + int realAnimationEndFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX).orElse(self.getRealAnimation().get().getCoord().getKeyframes().length - 1); + Vec3 toDestWorld = destLocation.subtract(startInWorld); + Vec3f toDestAnim = realAnimationCoord[realAnimationEndFrame].transform().translation(); + LivingEntity attackTarget = entitypatch.getTarget(); + + // Calculate Entity-Entity collide radius + float entityRadius = 0.0F; + + if (attackTarget != null) { + float reach = 0.0F; + + if (self.getRealAnimation().get() instanceof AttackAnimation attackAnimation) { + Optional reachOpt = attackAnimation.getProperty(AttackAnimationProperty.REACH); + + if (reachOpt.isPresent()) { + reach = reachOpt.get(); + } else { + AnimationPlayer player = entitypatch.getAnimator().getPlayerFor(self.getAccessor()); + + if (player != null) { + Phase phase = attackAnimation.getPhaseByTime(player.getElapsedTime()); + reach = entitypatch.getReach(phase.hand); + } + } + } + + entityRadius = (attackTarget.getBbWidth() + entitypatch.getOriginal().getBbWidth()) * 0.7F + reach; + } + + float worldLength = Math.max((float)toDestWorld.length() - entityRadius, 0.0F); + float animLength = toDestAnim.length(); + + float dot = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.INITIAL_LOOK_VEC_DOT, self.getRealAnimation()); + float lookLength = Mth.lerp(dot, animLength, worldLength); + float scale = Math.min(lookLength / animLength, 1.0F); + + if (self.isLinkAnimation()) { + scale *= coord[coord.length - 1].transform().translation().length() / animLength; + } + + int endFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX).orElse(coord.length - 1); + + for (int i = startFrame; i <= endFrame; i++) { + Vec3f translation = coord[i].transform().translation(); + translation.x *= scale; + + if (translation.z < 0.0F) { + translation.z *= scale; + } + } + + transformSheet.readFrom(transform); + } else { + transformSheet.readFrom(self.getCoord().copyAll()); + } + }; + + /** + * Trace the target entity's position (use it MODEL_COORD) + * - the location of the last keyframe is a basis to limit maximum distance + * - rotation is the direction toward a target entity + */ + public static final MoveCoordSetter TRACE_TARGET_LOCATION_ROTATION = (self, entitypatch, transformSheet) -> { + Vec3 destLocation = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER).orElse(NO_DEST).get(self, entitypatch); + + if (destLocation != null) { + TransformSheet transform = self.getCoord().copyAll(); + Keyframe[] coord = transform.getKeyframes(); + Keyframe[] realAnimationCoord = self.getRealAnimation().get().getCoord().getKeyframes(); + Vec3 startInWorld = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.BEGINNING_LOCATION, self.getRealAnimation()); + + if (startInWorld == null) { + startInWorld = entitypatch.getOriginal().position(); + } + + int startFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_START_KEYFRAME_INDEX).orElse(0); + int endFrame = self.isLinkAnimation() ? coord.length - 1 : self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX).orElse(coord.length - 1); + Vec3 toDestWorld = destLocation.subtract(startInWorld); + Vec3f toDestAnim = realAnimationCoord[endFrame].transform().translation(); + LivingEntity attackTarget = entitypatch.getTarget(); + + // Calculate Entity-Entity collide radius + float entityRadius = 0.0F; + + if (attackTarget != null) { + float reach = 0.0F; + + if (self.getRealAnimation().get() instanceof AttackAnimation attackAnimation) { + Optional reachOpt = attackAnimation.getProperty(AttackAnimationProperty.REACH); + + if (reachOpt.isPresent()) { + reach = reachOpt.get(); + } else { + AnimationPlayer player = entitypatch.getAnimator().getPlayerFor(self.getAccessor()); + + if (player != null) { + Phase phase = attackAnimation.getPhaseByTime(player.getElapsedTime()); + reach = entitypatch.getReach(phase.hand); + } + } + } + + entityRadius = (attackTarget.getBbWidth() + entitypatch.getOriginal().getBbWidth()) * 0.7F + reach; + } + + float worldLength = Math.max((float)toDestWorld.length() - entityRadius, 0.0F); + float animLength = toDestAnim.length(); + float scale = Math.min(worldLength / animLength, 1.0F); + + if (self.isLinkAnimation()) { + scale *= coord[endFrame].transform().translation().length() / animLength; + } + + for (int i = startFrame; i <= endFrame; i++) { + Vec3f translation = coord[i].transform().translation(); + translation.x *= scale; + + if (translation.z < 0.0F) { + translation.z *= scale; + } + } + + transformSheet.readFrom(transform); + } else { + transformSheet.readFrom(self.getCoord().copyAll()); + } + }; + + public static final MoveCoordSetter VEX_TRACE = (self, entitypatch, transformSheet) -> { + if (!self.isLinkAnimation()) { + TransformSheet transform = self.getCoord().copyAll(); + + if (entitypatch.getTarget() != null) { + Keyframe[] keyframes = transform.getKeyframes(); + Vec3 pos = entitypatch.getOriginal().position(); + Vec3 targetpos = entitypatch.getTarget().getEyePosition(); + double flyDistance = Math.max(5.0D, targetpos.subtract(pos).length() * 2); + + transform.forEach((index, keyframe) -> { + keyframe.transform().translation().scale((float)(flyDistance / Math.abs(keyframes[keyframes.length - 1].transform().translation().z))); + }); + + Vec3 toTarget = targetpos.subtract(pos); + float xRot = (float)-MathUtils.getXRotOfVector(toTarget); + float yRot = (float)MathUtils.getYRotOfVector(toTarget); + + entitypatch.setYRot(yRot); + + transform.forEach((index, keyframe) -> { + keyframe.transform().translation().rotateDegree(Vec3f.X_AXIS, xRot); + keyframe.transform().translation().rotateDegree(Vec3f.Y_AXIS, 180.0F - yRot); + keyframe.transform().translation().add(entitypatch.getOriginal().position()); + }); + + transformSheet.readFrom(transform); + } else { + transform.forEach((index, keyframe) -> { + keyframe.transform().translation().rotateDegree(Vec3f.Y_AXIS, 180.0F - entitypatch.getYRot()); + keyframe.transform().translation().add(entitypatch.getOriginal().position()); + }); + } + } + }; +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/ActionAnimation.java b/src/main/java/com/tiedup/remake/rig/anim/types/ActionAnimation.java new file mode 100644 index 0000000..9888c5a --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/ActionAnimation.java @@ -0,0 +1,405 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import net.minecraft.util.Mth; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.common.ForgeMod; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.AnimationPlayer; +import com.tiedup.remake.rig.anim.AnimationVariables; +import com.tiedup.remake.rig.anim.AnimationVariables.IndependentAnimationVariableKey; +import com.tiedup.remake.rig.anim.AnimationVariables.SharedAnimationVariableKey; +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.anim.Keyframe; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.TransformSheet; +import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty; +import com.tiedup.remake.rig.anim.property.AnimationProperty.AttackAnimationProperty; +import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier; +import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty; +import com.tiedup.remake.rig.anim.property.MoveCoordFunctions; +import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordGetter; +import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordSetter; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.anim.client.Layer; +import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties; +import com.tiedup.remake.rig.anim.client.property.JointMaskEntry; +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.util.TimePairList; +import com.tiedup.remake.rig.math.MathUtils; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; +import com.tiedup.remake.rig.patch.LocalPlayerPatch; +import yesman.epicfight.main.EpicFightSharedConstants; +import yesman.epicfight.network.EpicFightNetworkManager; +import yesman.epicfight.network.client.CPSyncPlayerAnimationPosition; +import yesman.epicfight.network.server.SPSyncAnimationPosition; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class ActionAnimation extends MainFrameAnimation { + public static final SharedAnimationVariableKey ACTION_ANIMATION_COORD = AnimationVariables.shared((animator) -> new TransformSheet(), false); + public static final IndependentAnimationVariableKey BEGINNING_LOCATION = AnimationVariables.independent((animator) -> animator.getEntityPatch().getOriginal().position(), true); + public static final IndependentAnimationVariableKey INITIAL_LOOK_VEC_DOT = AnimationVariables.independent((animator) -> 1.0F, true); + + public ActionAnimation(float transitionTime, AnimationAccessor accessor, AssetAccessor armature) { + this(transitionTime, Float.MAX_VALUE, accessor, armature); + } + + public ActionAnimation(float transitionTime, float postDelay, AnimationAccessor accessor, AssetAccessor armature) { + super(transitionTime, accessor, armature); + + this.stateSpectrumBlueprint.clear() + .newTimePair(0.0F, postDelay) + .addState(EntityState.MOVEMENT_LOCKED, true) + .addState(EntityState.UPDATE_LIVING_MOTION, false) + .addState(EntityState.CAN_BASIC_ATTACK, false) + .addState(EntityState.CAN_SKILL_EXECUTION, false) + .addState(EntityState.TURNING_LOCKED, true) + .newTimePair(0.0F, Float.MAX_VALUE) + .addState(EntityState.INACTION, true); + + this.addProperty(StaticAnimationProperty.FIXED_HEAD_ROTATION, true); + } + + /** + * For resourcepack animations + */ + public ActionAnimation(float transitionTime, float postDelay, String path, AssetAccessor armature) { + super(transitionTime, path, armature); + + this.stateSpectrumBlueprint.clear() + .newTimePair(0.0F, postDelay) + .addState(EntityState.MOVEMENT_LOCKED, true) + .addState(EntityState.UPDATE_LIVING_MOTION, false) + .addState(EntityState.CAN_BASIC_ATTACK, false) + .addState(EntityState.CAN_SKILL_EXECUTION, false) + .addState(EntityState.TURNING_LOCKED, true) + .newTimePair(0.0F, Float.MAX_VALUE) + .addState(EntityState.INACTION, true); + + this.addProperty(StaticAnimationProperty.FIXED_HEAD_ROTATION, true); + } + + @Override + public void putOnPlayer(AnimationPlayer animationPlayer, LivingEntityPatch entitypatch) { + if (entitypatch.shouldMoveOnCurrentSide(this)) { + MoveCoordSetter moveCoordSetter = this.getProperty(ActionAnimationProperty.COORD_SET_BEGIN).orElse(MoveCoordFunctions.RAW_COORD); + moveCoordSetter.set(this, entitypatch, entitypatch.getAnimator().getVariables().getOrDefaultSharedVariable(ACTION_ANIMATION_COORD)); + } + + super.putOnPlayer(animationPlayer, entitypatch); + } + + protected void initCoordVariables(LivingEntityPatch entitypatch) { + Vec3 start = entitypatch.getOriginal().position(); + + if (entitypatch.getTarget() != null) { + Vec3 targetTracePosition = entitypatch.getTarget().position(); + Vec3 toDestWorld = targetTracePosition.subtract(start); + float dot = Mth.clamp((float)toDestWorld.normalize().dot(MathUtils.getVectorForRotation(0.0F, entitypatch.getYRot())), 0.0F, 1.0F); + entitypatch.getAnimator().getVariables().put(INITIAL_LOOK_VEC_DOT, this.getAccessor(), dot); + } + + entitypatch.getAnimator().getVariables().put(BEGINNING_LOCATION, this.getAccessor(), start); + } + + @Override + public void begin(LivingEntityPatch entitypatch) { + entitypatch.cancelItemUse(); + + super.begin(entitypatch); + + if (entitypatch.shouldMoveOnCurrentSide(this)) { + entitypatch.beginAction(this); + + this.initCoordVariables(entitypatch); + + if (this.getProperty(ActionAnimationProperty.STOP_MOVEMENT).orElse(false)) { + entitypatch.getOriginal().setDeltaMovement(0.0D, entitypatch.getOriginal().getDeltaMovement().y, 0.0D); + entitypatch.getOriginal().xxa = 0.0F; + entitypatch.getOriginal().yya = 0.0F; + entitypatch.getOriginal().zza = 0.0F; + } + } + } + + @Override + public void tick(LivingEntityPatch entitypatch) { + super.tick(entitypatch); + + if (this.getProperty(ActionAnimationProperty.REMOVE_DELTA_MOVEMENT).orElse(false)) { + double gravity = this.getProperty(ActionAnimationProperty.MOVE_VERTICAL).orElse(false) ? 0.0D : entitypatch.getOriginal().getDeltaMovement().y; + entitypatch.getOriginal().setDeltaMovement(0.0D, gravity, 0.0D); + } + + this.move(entitypatch, this.getAccessor()); + } + + @Override + public void linkTick(LivingEntityPatch entitypatch, AssetAccessor linkAnimation) { + if (this.getProperty(ActionAnimationProperty.REMOVE_DELTA_MOVEMENT).orElse(false)) { + double gravity = this.getProperty(ActionAnimationProperty.MOVE_VERTICAL).orElse(false) ? 0.0D : entitypatch.getOriginal().getDeltaMovement().y; + entitypatch.getOriginal().setDeltaMovement(0.0D, gravity, 0.0D); + } + + this.move(entitypatch, linkAnimation); + } + + protected void move(LivingEntityPatch entitypatch, AssetAccessor animation) { + if (!this.validateMovement(entitypatch, animation)) { + return; + } + + float elapsedTime = entitypatch.getAnimator().getPlayerFor(this.getAccessor()).getElapsedTime(); + + if (this.getState(EntityState.INACTION, entitypatch, elapsedTime)) { + LivingEntity livingentity = entitypatch.getOriginal(); + Vec3 vec3 = this.getCoordVector(entitypatch, animation); + livingentity.move(MoverType.SELF, vec3); + + if (entitypatch.isLogicalClient()) { + EpicFightNetworkManager.sendToServer(new CPSyncPlayerAnimationPosition(livingentity.getId(), elapsedTime, livingentity.position(), animation.get().isLinkAnimation() ? 2 : 1)); + } else { + EpicFightNetworkManager.sendToAllPlayerTrackingThisEntity(new SPSyncAnimationPosition(livingentity.getId(), elapsedTime, livingentity.position(), animation.get().isLinkAnimation() ? 2 : 1), livingentity); + } + } + } + + protected boolean validateMovement(LivingEntityPatch entitypatch, AssetAccessor animation) { + if (!entitypatch.shouldMoveOnCurrentSide(this)) { + return false; + } + + if (animation.get().isLinkAnimation()) { + if (!this.getProperty(ActionAnimationProperty.MOVE_ON_LINK).orElse(true)) { + return false; + } else { + return this.shouldMove(0.0F); + } + } else { + return this.shouldMove(entitypatch.getAnimator().getPlayerFor(animation).getElapsedTime()); + } + } + + protected boolean shouldMove(float currentTime) { + if (this.properties.containsKey(ActionAnimationProperty.MOVE_TIME)) { + TimePairList moveTimes = this.getProperty(ActionAnimationProperty.MOVE_TIME).get(); + return moveTimes.isTimeInPairs(currentTime); + } else { + return true; + } + } + + @Override + public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch entitypatch, float time, float partialTicks) { + if (this.getProperty(ActionAnimationProperty.COORD).isEmpty()) { + this.correctRootJoint(animation, pose, entitypatch, time, partialTicks); + } + + super.modifyPose(animation, pose, entitypatch, time, partialTicks); + } + + public void correctRootJoint(DynamicAnimation animation, Pose pose, LivingEntityPatch entitypatch, float time, float partialTicks) { + JointTransform jt = pose.orElseEmpty("Root"); + Vec3f jointPosition = jt.translation(); + OpenMatrix4f toRootTransformApplied = entitypatch.getArmature().searchJointByName("Root").getLocalTransform().removeTranslation(); + OpenMatrix4f toOrigin = OpenMatrix4f.invert(toRootTransformApplied, null); + Vec3f worldPosition = OpenMatrix4f.transform3v(toRootTransformApplied, jointPosition, null); + worldPosition.x = 0.0F; + worldPosition.y = (this.getProperty(ActionAnimationProperty.MOVE_VERTICAL).orElse(false) && worldPosition.y > 0.0F) ? 0.0F : worldPosition.y; + worldPosition.z = 0.0F; + OpenMatrix4f.transform3v(toOrigin, worldPosition, worldPosition); + jointPosition.x = worldPosition.x; + jointPosition.y = worldPosition.y; + jointPosition.z = worldPosition.z; + } + + @Override + public void setLinkAnimation(AssetAccessor fromAnimation, Pose startPose, boolean isOnSameLayer, float transitionTimeModifier, LivingEntityPatch entitypatch, LinkAnimation dest) { + dest.resetNextStartTime(); + float playTime = this.getPlaySpeed(entitypatch, dest); + PlaybackSpeedModifier playSpeedModifier = this.getRealAnimation().get().getProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER).orElse(null); + + if (playSpeedModifier != null) { + playTime = playSpeedModifier.modify(this, entitypatch, playTime, 0.0F, playTime); + } + + playTime = Math.abs(playTime) * EpicFightSharedConstants.A_TICK; + + float linkTime = (transitionTimeModifier > 0.0F) ? transitionTimeModifier + this.transitionTime : this.transitionTime; + float totalTime = playTime * (int)Math.ceil(linkTime / playTime); + float nextStartTime = Math.max(0.0F, -transitionTimeModifier); + nextStartTime += totalTime - linkTime; + + dest.setNextStartTime(nextStartTime); + dest.getAnimationClip().reset(); + dest.setTotalTime(totalTime); + dest.setConnectedAnimations(fromAnimation, this.getAccessor()); + + Pose nextStartPose = this.getPoseByTime(entitypatch, nextStartTime, 1.0F); + + if (entitypatch.shouldMoveOnCurrentSide(this) && this.getProperty(ActionAnimationProperty.MOVE_ON_LINK).orElse(true)) { + this.correctRawZCoord(entitypatch, nextStartPose, nextStartTime); + } + + Map data1 = startPose.getJointTransformData(); + Map data2 = nextStartPose.getJointTransformData(); + Set joint1 = new HashSet<> (isOnSameLayer ? data1.keySet() : Set.of()); + Set joint2 = new HashSet<> (data2.keySet()); + + if (entitypatch.isLogicalClient()) { + JointMaskEntry entry = fromAnimation.get().getJointMaskEntry(entitypatch, false).orElse(null); + JointMaskEntry entry2 = this.getJointMaskEntry(entitypatch, true).orElse(null); + + if (entry != null && entitypatch.isLogicalClient()) { + joint1.removeIf((jointName) -> entry.isMasked(fromAnimation.get().getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ? + entitypatch.getClientAnimator().currentMotion() : entitypatch.getClientAnimator().currentCompositeMotion(), jointName)); + } + + if (entry2 != null && entitypatch.isLogicalClient()) { + joint2.removeIf((jointName) -> entry2.isMasked(this.getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ? + entitypatch.getCurrentLivingMotion() : entitypatch.currentCompositeMotion, jointName)); + } + } + + joint1.addAll(joint2); + + if (linkTime != totalTime) { + Pose pose = this.getPoseByTime(entitypatch, 0.0F, 0.0F); + Map poseData = pose.getJointTransformData(); + + if (entitypatch.shouldMoveOnCurrentSide(this) && this.getProperty(ActionAnimationProperty.MOVE_ON_LINK).orElse(true)) { + this.correctRawZCoord(entitypatch, pose, 0.0F); + } + + for (String jointName : joint1) { + Keyframe[] keyframes = new Keyframe[3]; + keyframes[0] = new Keyframe(0.0F, data1.getOrDefault(jointName, JointTransform.empty())); + keyframes[1] = new Keyframe(linkTime, poseData.getOrDefault(jointName, JointTransform.empty())); + keyframes[2] = new Keyframe(totalTime, data2.getOrDefault(jointName, JointTransform.empty())); + + TransformSheet sheet = new TransformSheet(keyframes); + dest.getAnimationClip().addJointTransform(jointName, sheet); + } + } else { + for (String jointName : joint1) { + Keyframe[] keyframes = new Keyframe[2]; + keyframes[0] = new Keyframe(0.0F, data1.getOrDefault(jointName, JointTransform.empty())); + keyframes[1] = new Keyframe(totalTime, data2.getOrDefault(jointName, JointTransform.empty())); + + TransformSheet sheet = new TransformSheet(keyframes); + dest.getAnimationClip().addJointTransform(jointName, sheet); + } + } + + dest.loadCoord(null); + + this.getProperty(ActionAnimationProperty.COORD).ifPresent((coord) -> { + Keyframe[] keyframes = new Keyframe[2]; + keyframes[0] = new Keyframe(0.0F, JointTransform.empty()); + keyframes[1] = new Keyframe(totalTime, coord.getKeyframes()[0].transform()); + + TransformSheet sheet = new TransformSheet(keyframes); + dest.loadCoord(sheet); + }); + + if (entitypatch.shouldMoveOnCurrentSide(this)) { + MoveCoordSetter moveCoordSetter = this.getProperty(ActionAnimationProperty.COORD_SET_BEGIN).orElse(MoveCoordFunctions.RAW_COORD); + moveCoordSetter.set(dest, entitypatch, entitypatch.getAnimator().getVariables().getOrDefaultSharedVariable(ACTION_ANIMATION_COORD)); + } + } + + public void correctRawZCoord(LivingEntityPatch entitypatch, Pose pose, float poseTime) { + JointTransform jt = pose.orElseEmpty("Root"); + + if (this.getProperty(ActionAnimationProperty.COORD).isEmpty()) { + TransformSheet coordTransform = this.getTransfroms().get("Root"); + jt.translation().add(0.0F, 0.0F, coordTransform.getInterpolatedTranslation(poseTime).z); + } + } + + /** + * Get an expected movement vector in specific time + * + * @param entitypatch + * @param elapseTime + * @return + */ + public Vec3 getExpectedMovement(LivingEntityPatch entitypatch, float elapseTime) { + this.initCoordVariables(entitypatch); + + TransformSheet coordTransform = new TransformSheet(); + MoveCoordSetter moveCoordSetter = this.getProperty(ActionAnimationProperty.COORD_SET_BEGIN).orElse(MoveCoordFunctions.RAW_COORD); + moveCoordSetter.set(this, entitypatch, coordTransform); + + MoveCoordGetter moveGetter = this.getProperty(ActionAnimationProperty.COORD_GET).orElse(MoveCoordFunctions.MODEL_COORD); + Vec3f move = moveGetter.get(this, entitypatch, coordTransform, 0.0F, elapseTime); + + return move.toDoubleVector(); + } + + protected Vec3 getCoordVector(LivingEntityPatch entitypatch, AssetAccessor animation) { + AnimationPlayer player = entitypatch.getAnimator().getPlayerFor(animation); + TimePairList coordUpdateTime = this.getProperty(ActionAnimationProperty.COORD_UPDATE_TIME).orElse(null); + boolean inUpdateTime = coordUpdateTime == null || coordUpdateTime.isTimeInPairs(player.getElapsedTime()); + boolean getRawCoord = this.getProperty(AttackAnimationProperty.FIXED_MOVE_DISTANCE).orElse(!inUpdateTime); + TransformSheet transformSheet = entitypatch.getAnimator().getVariables().getOrDefaultSharedVariable(ACTION_ANIMATION_COORD); + MoveCoordSetter moveCoordsetter = getRawCoord ? MoveCoordFunctions.RAW_COORD : this.getProperty(ActionAnimationProperty.COORD_SET_TICK).orElse(null); + + if (moveCoordsetter != null) { + moveCoordsetter.set(animation.get(), entitypatch, transformSheet); + } + + boolean hasNoGravity = entitypatch.getOriginal().isNoGravity(); + boolean moveVertical = this.getProperty(ActionAnimationProperty.MOVE_VERTICAL).orElse(this.getProperty(ActionAnimationProperty.COORD).isPresent()); + MoveCoordGetter moveGetter = getRawCoord ? MoveCoordFunctions.MODEL_COORD : this.getProperty(ActionAnimationProperty.COORD_GET).orElse(MoveCoordFunctions.MODEL_COORD); + + Vec3f move = moveGetter.get(animation.get(), entitypatch, transformSheet, player.getPrevElapsedTime(), player.getElapsedTime()); + LivingEntity livingentity = entitypatch.getOriginal(); + Vec3 motion = livingentity.getDeltaMovement(); + + this.getProperty(ActionAnimationProperty.NO_GRAVITY_TIME).ifPresentOrElse((noGravityTime) -> { + if (noGravityTime.isTimeInPairs(animation.get().isLinkAnimation() ? 0.0F : player.getElapsedTime())) { + livingentity.setDeltaMovement(motion.x, 0.0D, motion.z); + } else { + move.y = 0.0F; + } + }, () -> { + if (moveVertical && move.y > 0.0F && !hasNoGravity) { + double gravity = livingentity.getAttribute(ForgeMod.ENTITY_GRAVITY.get()).getValue(); + livingentity.setDeltaMovement(motion.x, motion.y < 0.0D ? motion.y + gravity : 0.0D, motion.z); + } + }); + + if (!moveVertical) { + move.y = 0.0F; + } + + if (inUpdateTime) { + this.getProperty(ActionAnimationProperty.ENTITY_YROT_PROVIDER).ifPresent((entityYRotProvider) -> { + float yRot = entityYRotProvider.get(animation.get(), entitypatch); + entitypatch.setYRot(yRot); + }); + } + + return move.toDoubleVector(); + } + + @OnlyIn(Dist.CLIENT) + public boolean shouldPlayerMove(LocalPlayerPatch playerpatch) { + return playerpatch.isLogicalClient(); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/AttackAnimation.java b/src/main/java/com/tiedup/remake/rig/anim/types/AttackAnimation.java new file mode 100644 index 0000000..abe50f5 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/AttackAnimation.java @@ -0,0 +1,570 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.datafixers.util.Pair; + +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.entity.PartEntity; +import net.minecraftforge.registries.RegistryObject; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.AnimationPlayer; +import com.tiedup.remake.rig.anim.AnimationVariables; +import com.tiedup.remake.rig.anim.AnimationVariables.SharedAnimationVariableKey; +import com.tiedup.remake.rig.armature.Joint; +import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty; +import com.tiedup.remake.rig.anim.property.AnimationProperty.AttackAnimationProperty; +import com.tiedup.remake.rig.anim.property.AnimationProperty.AttackPhaseProperty; +import com.tiedup.remake.rig.anim.property.MoveCoordFunctions; +import com.tiedup.remake.rig.anim.types.EntityState.StateFactor; +import com.tiedup.remake.rig.asset.AssetAccessor; +import yesman.epicfight.api.collider.Collider; +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.util.AttackResult; +import com.tiedup.remake.rig.util.HitEntityList; +import com.tiedup.remake.rig.math.MathUtils; +import com.tiedup.remake.rig.math.ValueModifier; +import yesman.epicfight.main.EpicFightSharedConstants; +import yesman.epicfight.particle.HitParticleType; +import com.tiedup.remake.rig.patch.HumanoidMobPatch; +import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.patch.PlayerPatch; +import com.tiedup.remake.rig.patch.ServerPlayerPatch; +import yesman.epicfight.world.damagesource.EpicFightDamageSource; +import yesman.epicfight.world.damagesource.EpicFightDamageSources; +import yesman.epicfight.world.entity.eventlistener.AttackEndEvent; +import yesman.epicfight.world.entity.eventlistener.AttackPhaseEndEvent; +import yesman.epicfight.world.entity.eventlistener.PlayerEventListener.EventType; + +public class AttackAnimation extends ActionAnimation { + /** Entities that collided **/ + public static final SharedAnimationVariableKey> ATTACK_TRIED_ENTITIES = AnimationVariables.shared((animator) -> Lists.newArrayList(), false); + /** Entities that actually hurt **/ + public static final SharedAnimationVariableKey> ACTUALLY_HIT_ENTITIES = AnimationVariables.shared((animator) -> Lists.newArrayList(), false); + + public final Phase[] phases; + + public AttackAnimation(float transitionTime, float antic, float preDelay, float contact, float recovery, @Nullable Collider collider, Joint colliderJoint, AnimationAccessor accessor, AssetAccessor armature) { + this(transitionTime, accessor, armature, new Phase(0.0F, antic, preDelay, contact, recovery, Float.MAX_VALUE, colliderJoint, collider)); + } + + public AttackAnimation(float transitionTime, float antic, float preDelay, float contact, float recovery, InteractionHand hand, @Nullable Collider collider, Joint colliderJoint, AnimationAccessor accessor, AssetAccessor armature) { + this(transitionTime, accessor, armature, new Phase(0.0F, antic, preDelay, contact, recovery, Float.MAX_VALUE, hand, colliderJoint, collider)); + } + + public AttackAnimation(float transitionTime, AnimationAccessor accessor, AssetAccessor armature, Phase... phases) { + super(transitionTime, accessor, armature); + + this.addProperty(ActionAnimationProperty.COORD_SET_BEGIN, MoveCoordFunctions.TRACE_TARGET_DISTANCE); + this.addProperty(ActionAnimationProperty.COORD_SET_TICK, MoveCoordFunctions.TRACE_TARGET_DISTANCE); + this.addProperty(ActionAnimationProperty.COORD_GET, MoveCoordFunctions.MODEL_COORD); + this.addProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER, MoveCoordFunctions.ATTACK_TARGET_LOCATION); + this.addProperty(ActionAnimationProperty.ENTITY_YROT_PROVIDER, MoveCoordFunctions.MOB_ATTACK_TARGET_LOOK); + this.addProperty(ActionAnimationProperty.STOP_MOVEMENT, true); + + this.phases = phases; + this.stateSpectrumBlueprint.clear(); + + for (Phase phase : phases) { + if (!phase.noStateBind) { + this.bindPhaseState(phase); + } + } + } + + /** + * For resourcepack animation + */ + public AttackAnimation(float convertTime, float antic, float preDelay, float contact, float recovery, InteractionHand hand, @Nullable Collider collider, Joint colliderJoint, String path, AssetAccessor armature) { + this(convertTime, path, armature, new Phase(0.0F, antic, preDelay, contact, recovery, Float.MAX_VALUE, hand, colliderJoint, collider)); + } + + /** + * For resourcepack animation + */ + public AttackAnimation(float convertTime, String path, AssetAccessor armature, Phase... phases) { + super(convertTime, 0.0F, path, armature); + + this.addProperty(ActionAnimationProperty.COORD_SET_BEGIN, MoveCoordFunctions.TRACE_TARGET_DISTANCE); + this.addProperty(ActionAnimationProperty.COORD_SET_TICK, MoveCoordFunctions.TRACE_TARGET_DISTANCE); + this.addProperty(ActionAnimationProperty.COORD_GET, MoveCoordFunctions.MODEL_COORD); + this.addProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER, MoveCoordFunctions.ATTACK_TARGET_LOCATION); + this.addProperty(ActionAnimationProperty.ENTITY_YROT_PROVIDER, MoveCoordFunctions.MOB_ATTACK_TARGET_LOOK); + this.addProperty(ActionAnimationProperty.STOP_MOVEMENT, true); + + this.phases = phases; + this.stateSpectrumBlueprint.clear(); + + for (Phase phase : phases) { + if (!phase.noStateBind) { + this.bindPhaseState(phase); + } + } + } + + protected void bindPhaseState(Phase phase) { + float preDelay = phase.preDelay; + + this.stateSpectrumBlueprint + .newTimePair(phase.start, preDelay) + .addState(EntityState.PHASE_LEVEL, 1) + .newTimePair(phase.start, phase.contact) + .addState(EntityState.CAN_SKILL_EXECUTION, false) + .newTimePair(phase.start, phase.recovery) + .addState(EntityState.MOVEMENT_LOCKED, true) + .addState(EntityState.UPDATE_LIVING_MOTION, false) + .addState(EntityState.CAN_BASIC_ATTACK, false) + .newTimePair(phase.start, phase.end) + .addState(EntityState.INACTION, true) + .newTimePair(phase.antic, phase.end) + .addState(EntityState.TURNING_LOCKED, true) + .newTimePair(preDelay, phase.contact) + .addState(EntityState.ATTACKING, true) + .addState(EntityState.PHASE_LEVEL, 2) + .newTimePair(phase.contact, phase.end) + .addState(EntityState.PHASE_LEVEL, 3) + ; + } + + @Override + public void begin(LivingEntityPatch entitypatch) { + super.begin(entitypatch); + + entitypatch.setLastAttackSuccess(false); + } + + @Override + public void linkTick(LivingEntityPatch entitypatch, AssetAccessor linkAnimation) { + super.linkTick(entitypatch, linkAnimation); + + if (!entitypatch.isLogicalClient()) { + this.attackTick(entitypatch, linkAnimation); + } + } + + @Override + public void tick(LivingEntityPatch entitypatch) { + super.tick(entitypatch); + + if (!entitypatch.isLogicalClient()) { + this.attackTick(entitypatch, this.getAccessor()); + } + } + + @Override + public void end(LivingEntityPatch entitypatch, AssetAccessor nextAnimation, boolean isEnd) { + super.end(entitypatch, nextAnimation, isEnd); + + if (entitypatch instanceof ServerPlayerPatch playerpatch) { + if (isEnd) { + playerpatch.getEventListener().triggerEvents(EventType.ATTACK_ANIMATION_END_EVENT, new AttackEndEvent(playerpatch, this.getAccessor())); + } + + AnimationPlayer player = entitypatch.getAnimator().getPlayerFor(this.getAccessor()); + float elapsedTime = player.getElapsedTime(); + EntityState state = this.getState(entitypatch, elapsedTime); + + if (!isEnd && state.attacking()) { + playerpatch.getEventListener().triggerEvents(EventType.ATTACK_PHASE_END_EVENT, new AttackPhaseEndEvent(playerpatch, this.getAccessor(), this.getPhaseByTime(elapsedTime), this.getPhaseOrderByTime(elapsedTime))); + } + } + + if (entitypatch instanceof HumanoidMobPatch mobpatch && entitypatch.isLogicalClient()) { + Mob entity = mobpatch.getOriginal(); + + if (entity.getTarget() != null && !entity.getTarget().isAlive()) { + entity.setTarget(null); + } + } + } + + protected void attackTick(LivingEntityPatch entitypatch, AssetAccessor animation) { + AnimationPlayer player = entitypatch.getAnimator().getPlayerFor(this.getAccessor()); + float prevElapsedTime = player.getPrevElapsedTime(); + float elapsedTime = player.getElapsedTime(); + EntityState prevState = animation.get().getState(entitypatch, prevElapsedTime); + EntityState state = animation.get().getState(entitypatch, elapsedTime); + Phase phase = this.getPhaseByTime(animation.get().isLinkAnimation() ? 0.0F : elapsedTime); + + if (prevState.attacking() || state.attacking() || (prevState.getLevel() <= 2 && state.getLevel() > 2)) { + if (!prevState.attacking() || (phase != this.getPhaseByTime(prevElapsedTime) && (state.attacking() || (prevState.getLevel() <= 2 && state.getLevel() > 2)))) { + entitypatch.onStrike(this, phase.hand); + entitypatch.playSound(this.getSwingSound(entitypatch, phase), 0.0F, 0.0F); + entitypatch.removeHurtEntities(); + } + + this.hurtCollidingEntities(entitypatch, prevElapsedTime, elapsedTime, prevState, state, phase); + + if ((!state.attacking() || elapsedTime >= this.getTotalTime()) && entitypatch instanceof ServerPlayerPatch playerpatch) { + playerpatch.getEventListener().triggerEvents(EventType.ATTACK_PHASE_END_EVENT, new AttackPhaseEndEvent(playerpatch, this.getAccessor(), phase, this.getPhaseOrderByTime(elapsedTime))); + } + } + } + + protected void hurtCollidingEntities(LivingEntityPatch entitypatch, float prevElapsedTime, float elapsedTime, EntityState prevState, EntityState state, Phase phase) { + LivingEntity entity = entitypatch.getOriginal(); + float prevPoseTime = prevState.attacking() ? prevElapsedTime : phase.preDelay; + float poseTime = state.attacking() ? elapsedTime : phase.contact; + List list = this.getPhaseByTime(elapsedTime).getCollidingEntities(entitypatch, this, prevPoseTime, poseTime, this.getPlaySpeed(entitypatch, this)); + + if (!list.isEmpty()) { + HitEntityList hitEntities = new HitEntityList(entitypatch, list, phase.getProperty(AttackPhaseProperty.HIT_PRIORITY).orElse(HitEntityList.Priority.DISTANCE)); + int maxStrikes = this.getMaxStrikes(entitypatch, phase); + + while (entitypatch.getCurrentlyActuallyHitEntities().size() < maxStrikes && hitEntities.next()) { + Entity target = hitEntities.getEntity(); + LivingEntity trueEntity = this.getTrueEntity(target); + + if (trueEntity != null && trueEntity.isAlive() && !entitypatch.getCurrentlyAttackTriedEntities().contains(trueEntity) && !entitypatch.isTargetInvulnerable(target)) { + if (target instanceof LivingEntity || target instanceof PartEntity) { + AABB aabb = target.getBoundingBox(); + + if (MathUtils.canBeSeen(target, entity, target.position().distanceTo(entity.getEyePosition()) + aabb.getCenter().distanceTo(new Vec3(aabb.maxX, aabb.maxY, aabb.maxZ)))) { + EpicFightDamageSource damagesource = this.getEpicFightDamageSource(entitypatch, target, phase); + int prevInvulTime = target.invulnerableTime; + target.invulnerableTime = 0; + + AttackResult attackResult = entitypatch.attack(damagesource, target, phase.hand); + target.invulnerableTime = prevInvulTime; + + if (attackResult.resultType.dealtDamage()) { + SoundEvent hitSound = this.getHitSound(entitypatch, phase); + + if (hitSound != null) { + target.level().playSound(null, target.getX(), target.getY(), target.getZ(), this.getHitSound(entitypatch, phase), target.getSoundSource(), 1.0F, 1.0F); + } + + this.spawnHitParticle((ServerLevel)target.level(), entitypatch, target, phase); + } + + entitypatch.getCurrentlyAttackTriedEntities().add(trueEntity); + + if (attackResult.resultType.shouldCount()) { + entitypatch.getCurrentlyActuallyHitEntities().add(trueEntity); + } + } + } + } + } + } + } + + public LivingEntity getTrueEntity(Entity entity) { + if (entity instanceof LivingEntity livingEntity) { + return livingEntity; + } else if (entity instanceof PartEntity partEntity) { + Entity parentEntity = partEntity.getParent(); + + if (parentEntity instanceof LivingEntity livingEntity) { + return livingEntity; + } + } + + return null; + } + + protected int getMaxStrikes(LivingEntityPatch entitypatch, Phase phase) { + return phase.getProperty(AttackPhaseProperty.MAX_STRIKES_MODIFIER) + .map(valueModifier -> (int)ValueModifier.calculator().attach(valueModifier).getResult(entitypatch.getMaxStrikes(phase.hand))) + .orElse(entitypatch.getMaxStrikes(phase.hand)); + } + + protected SoundEvent getSwingSound(LivingEntityPatch entitypatch, Phase phase) { + return phase.getProperty(AttackPhaseProperty.SWING_SOUND).orElse(entitypatch.getSwingSound(phase.hand)); + } + + protected SoundEvent getHitSound(LivingEntityPatch entitypatch, Phase phase) { + return phase.getProperty(AttackPhaseProperty.HIT_SOUND).orElse(entitypatch.getWeaponHitSound(phase.hand)); + } + + public EpicFightDamageSource getEpicFightDamageSource(LivingEntityPatch entitypatch, Entity target, Phase phase) { + return this.getEpicFightDamageSource(entitypatch.getDamageSource(this.getAccessor(), phase.hand), entitypatch, target, phase); + } + + public EpicFightDamageSource getEpicFightDamageSource(DamageSource originalSource, LivingEntityPatch entitypatch, Entity target, Phase phase) { + if (phase == null) { + phase = this.getPhaseByTime(entitypatch.getAnimator().getPlayerFor(this.getAccessor()).getElapsedTime()); + } + + EpicFightDamageSource epicfightSource; + + if (originalSource instanceof EpicFightDamageSource epicfightDamageSource) { + epicfightSource = epicfightDamageSource; + } else { + epicfightSource = EpicFightDamageSources.fromVanillaDamageSource(originalSource).setAnimation(this.getAccessor()); + } + + phase.getProperty(AttackPhaseProperty.DAMAGE_MODIFIER).ifPresent(opt -> { + epicfightSource.attachDamageModifier(opt); + }); + + phase.getProperty(AttackPhaseProperty.ARMOR_NEGATION_MODIFIER).ifPresent(opt -> { + epicfightSource.attachArmorNegationModifier(opt); + }); + + phase.getProperty(AttackPhaseProperty.IMPACT_MODIFIER).ifPresent(opt -> { + epicfightSource.attachImpactModifier(opt); + }); + + phase.getProperty(AttackPhaseProperty.STUN_TYPE).ifPresent(opt -> { + epicfightSource.setStunType(opt); + }); + + phase.getProperty(AttackPhaseProperty.SOURCE_TAG).ifPresent(opt -> { + opt.forEach(epicfightSource::addRuntimeTag); + }); + + phase.getProperty(AttackPhaseProperty.EXTRA_DAMAGE).ifPresent(opt -> { + opt.forEach(epicfightSource::addExtraDamage); + }); + + phase.getProperty(AttackPhaseProperty.SOURCE_LOCATION_PROVIDER).ifPresentOrElse(opt -> { + epicfightSource.setInitialPosition(opt.apply(entitypatch)); + }, () -> { + epicfightSource.setInitialPosition(entitypatch.getOriginal().position()); + }); + + return epicfightSource; + } + + protected void spawnHitParticle(ServerLevel world, LivingEntityPatch attacker, Entity hit, Phase phase) { + Optional> particleOptional = phase.getProperty(AttackPhaseProperty.PARTICLE); + HitParticleType particle = particleOptional.isPresent() ? particleOptional.get().get() : attacker.getWeaponHitParticle(phase.hand); + particle.spawnParticleWithArgument(world, null, null, hit, attacker.getOriginal()); + } + + @Override + public float getPlaySpeed(LivingEntityPatch entitypatch, DynamicAnimation animation) { + if (entitypatch instanceof PlayerPatch playerpatch) { + Phase phase = this.getPhaseByTime(playerpatch.getAnimator().getPlayerFor(this.getAccessor()).getElapsedTime()); + float speedFactor = this.getProperty(AttackAnimationProperty.ATTACK_SPEED_FACTOR).orElse(1.0F); + Optional property = this.getProperty(AttackAnimationProperty.BASIS_ATTACK_SPEED); + float correctedSpeed = property.map((value) -> playerpatch.getAttackSpeed(phase.hand) / value).orElse(this.getTotalTime() * playerpatch.getAttackSpeed(phase.hand)); + correctedSpeed = Math.round(correctedSpeed * 1000.0F) / 1000.0F; + + return 1.0F + (correctedSpeed - 1.0F) * speedFactor; + } + + return 1.0F; + } + + @SuppressWarnings("unchecked") + public A addProperty(AttackPhaseProperty propertyType, V value) { + return (A)this.addProperty(propertyType, value, 0); + } + + @SuppressWarnings("unchecked") + public A addProperty(AttackPhaseProperty propertyType, V value, int index) { + this.phases[index].addProperty(propertyType, value); + return (A)this; + } + + public A removeProperty(AttackPhaseProperty propertyType) { + return this.removeProperty(propertyType, 0); + } + + @SuppressWarnings("unchecked") + public A removeProperty(AttackPhaseProperty propertyType, int index) { + this.phases[index].removeProperty(propertyType); + return (A)this; + } + + public Phase getPhaseByTime(float elapsedTime) { + Phase currentPhase = null; + + for (Phase phase : this.phases) { + currentPhase = phase; + + if (phase.end > elapsedTime) { + break; + } + } + + return currentPhase; + } + + public int getPhaseOrderByTime(float elapsedTime) { + int i = 0; + + for (Phase phase : this.phases) { + if (phase.end > elapsedTime) { + break; + } + + i++; + } + + return i; + } + + @Override + public Object getModifiedLinkState(StateFactor factor, Object val, LivingEntityPatch entitypatch, float elapsedTime) { + if (factor == EntityState.ATTACKING && elapsedTime < this.getPlaySpeed(entitypatch, this) * EpicFightSharedConstants.A_TICK) { + return false; + } + + return val; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void renderDebugging(PoseStack poseStack, MultiBufferSource buffer, LivingEntityPatch entitypatch, float playbackTime, float partialTicks) { + AnimationPlayer animPlayer = entitypatch.getAnimator().getPlayerFor(this.getAccessor()); + float prevElapsedTime = animPlayer.getPrevElapsedTime(); + float elapsedTime = animPlayer.getElapsedTime(); + Phase phase = this.getPhaseByTime(playbackTime); + + for (Pair colliderInfo : phase.colliders) { + Collider collider = colliderInfo.getSecond(); + + if (collider == null) { + collider = entitypatch.getColliderMatching(phase.hand); + } + + collider.draw(poseStack, buffer, entitypatch, this, colliderInfo.getFirst(), prevElapsedTime, elapsedTime, partialTicks, this.getPlaySpeed(entitypatch, this)); + } + } + + public static class JointColliderPair extends Pair { + public JointColliderPair(Joint first, Collider second) { + super(first, second); + } + + public static JointColliderPair of(Joint joint, Collider collider) { + return new JointColliderPair(joint, collider); + } + } + + public static class Phase { + private final Map, Object> properties = Maps.newHashMap(); + public final float start; + public final float antic; + public final float preDelay; + public final float contact; + public final float recovery; + public final float end; + public final InteractionHand hand; + public JointColliderPair[] colliders; + + //public final Joint first; + //public final Collider second; + + public final boolean noStateBind; + + public Phase(float start, float antic, float contact, float recovery, float end, Joint joint, Collider collider) { + this(start, antic, contact, recovery, end, InteractionHand.MAIN_HAND, joint, collider); + } + + public Phase(float start, float antic, float contact, float recovery, float end, InteractionHand hand, Joint joint, Collider collider) { + this(start, antic, antic, contact, recovery, end, hand, joint, collider); + } + + public Phase(float start, float antic, float preDelay, float contact, float recovery, float end, Joint joint, Collider collider) { + this(start, antic, preDelay, contact, recovery, end, InteractionHand.MAIN_HAND, joint, collider); + } + + public Phase(float start, float antic, float preDelay, float contact, float recovery, float end, InteractionHand hand, Joint joint, Collider collider) { + this(start, antic, preDelay, contact, recovery, end, false, hand, joint, collider); + } + + public Phase(InteractionHand hand, Joint joint, Collider collider) { + this(0, 0, 0, 0, 0, 0, true, hand, joint, collider); + } + + public Phase(float start, float antic, float preDelay, float contact, float recovery, float end, boolean noStateBind, InteractionHand hand, Joint joint, Collider collider) { + this(start, antic, preDelay, contact, recovery, end, noStateBind, hand, JointColliderPair.of(joint, collider)); + } + + public Phase(float start, float antic, float preDelay, float contact, float recovery, float end, InteractionHand hand, JointColliderPair... colliders) { + this(start, antic, preDelay, contact, recovery, end, false, hand, colliders); + } + + public Phase(float start, float antic, float preDelay, float contact, float recovery, float end, boolean noStateBind, InteractionHand hand, JointColliderPair... colliders) { + if (start > end) { + throw new IllegalArgumentException("Phase create exception: Start time is bigger than end time"); + } + + this.start = start; + this.antic = antic; + this.preDelay = preDelay; + this.contact = contact; + this.recovery = recovery; + this.end = end; + this.colliders = colliders; + this.hand = hand; + this.noStateBind = noStateBind; + } + + public Phase addProperty(AttackPhaseProperty propertyType, V value) { + this.properties.put(propertyType, value); + return this; + } + + public Phase removeProperty(AttackPhaseProperty propertyType) { + this.properties.remove(propertyType); + return this; + } + + public void addProperties(Set, Object>> set) { + for(Map.Entry, Object> entry : set) { + this.properties.put(entry.getKey(), entry.getValue()); + } + } + + @SuppressWarnings("unchecked") + public Optional getProperty(AttackPhaseProperty propertyType) { + return (Optional) Optional.ofNullable(this.properties.get(propertyType)); + } + + public List getCollidingEntities(LivingEntityPatch entitypatch, AttackAnimation animation, float prevElapsedTime, float elapsedTime, float attackSpeed) { + Set entities = Sets.newHashSet(); + + for (Pair colliderInfo : this.colliders) { + Collider collider = colliderInfo.getSecond(); + + if (collider == null) { + collider = entitypatch.getColliderMatching(this.hand); + } + + entities.addAll(collider.updateAndSelectCollideEntity(entitypatch, animation, prevElapsedTime, elapsedTime, colliderInfo.getFirst(), attackSpeed)); + } + + return new ArrayList<>(entities); + } + + public JointColliderPair[] getColliders() { + return this.colliders; + } + + public InteractionHand getHand() { + return this.hand; + } + } +} diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/ConcurrentLinkAnimation.java b/src/main/java/com/tiedup/remake/rig/anim/types/ConcurrentLinkAnimation.java new file mode 100644 index 0000000..aed93ea --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/ConcurrentLinkAnimation.java @@ -0,0 +1,179 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import java.util.Optional; + +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import com.tiedup.remake.rig.anim.AnimationClip; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.types.EntityState.StateFactor; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.anim.client.Layer; +import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties; +import com.tiedup.remake.rig.anim.client.property.JointMaskEntry; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +@OnlyIn(Dist.CLIENT) +public class ConcurrentLinkAnimation extends DynamicAnimation implements AnimationAccessor { + protected AssetAccessor nextAnimation; + protected AssetAccessor currentAnimation; + protected float startsAt; + + public ConcurrentLinkAnimation() { + this.animationClip = new AnimationClip(); + } + + public void acceptFrom(AssetAccessor currentAnimation, AssetAccessor nextAnimation, float time) { + this.currentAnimation = currentAnimation; + this.nextAnimation = nextAnimation; + this.startsAt = time; + this.setTotalTime(nextAnimation.get().getTransitionTime()); + } + + @Override + public void tick(LivingEntityPatch entitypatch) { + this.nextAnimation.get().linkTick(entitypatch, this); + } + + @Override + public void end(LivingEntityPatch entitypatch, AssetAccessor nextAnimation, boolean isEnd) { + if (!isEnd) { + this.nextAnimation.get().end(entitypatch, nextAnimation, isEnd); + } else { + if (this.startsAt > 0.0F) { + entitypatch.getAnimator().getPlayer(this).ifPresent(player -> { + player.setElapsedTime(this.startsAt); + player.markDoNotResetTime(); + }); + + this.startsAt = 0.0F; + } + } + } + + @Override + public EntityState getState(LivingEntityPatch entitypatch, float time) { + return this.nextAnimation.get().getState(entitypatch, 0.0F); + } + + @Override + public T getState(StateFactor stateFactor, LivingEntityPatch entitypatch, float time) { + return this.nextAnimation.get().getState(stateFactor, entitypatch, 0.0F); + } + + @Override + public Pose getPoseByTime(LivingEntityPatch entitypatch, float time, float partialTicks) { + float elapsed = time + this.startsAt; + float currentElapsed = elapsed % this.currentAnimation.get().getTotalTime(); + float nextElapsed = elapsed % this.nextAnimation.get().getTotalTime(); + Pose currentAnimPose = this.currentAnimation.get().getPoseByTime(entitypatch, currentElapsed, 1.0F); + Pose nextAnimPose = this.nextAnimation.get().getPoseByTime(entitypatch, nextElapsed, 1.0F); + float interpolate = time / this.getTotalTime(); + + Pose interpolatedPose = Pose.interpolatePose(currentAnimPose, nextAnimPose, interpolate); + JointMaskEntry maskEntry = this.nextAnimation.get().getJointMaskEntry(entitypatch, true).orElse(null); + + if (maskEntry != null && entitypatch.isLogicalClient()) { + interpolatedPose.disableJoint((entry) -> + maskEntry.isMasked( + this.nextAnimation.get().getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ? entitypatch.getClientAnimator().currentMotion() : entitypatch.getClientAnimator().currentCompositeMotion() + , entry.getKey() + )); + } + + return interpolatedPose; + } + + @Override + public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch entitypatch, float time, float partialTicks) { + this.nextAnimation.get().modifyPose(this, pose, entitypatch, time, partialTicks); + } + + @Override + public float getPlaySpeed(LivingEntityPatch entitypatch, DynamicAnimation animation) { + return this.nextAnimation.get().getPlaySpeed(entitypatch, animation); + } + + public void setNextAnimation(AnimationAccessor animation) { + this.nextAnimation = animation; + } + + @OnlyIn(Dist.CLIENT) + @Override + public Optional getJointMaskEntry(LivingEntityPatch entitypatch, boolean useCurrentMotion) { + return this.nextAnimation.get().getJointMaskEntry(entitypatch, useCurrentMotion); + } + + @Override + public boolean isMainFrameAnimation() { + return this.nextAnimation.get().isMainFrameAnimation(); + } + + @Override + public boolean isReboundAnimation() { + return this.nextAnimation.get().isReboundAnimation(); + } + + @Override + public AssetAccessor getRealAnimation() { + return this.nextAnimation; + } + + @Override + public String toString() { + return "ConcurrentLinkAnimation: Mix " + this.currentAnimation + " and " + this.nextAnimation; + } + + @Override + public AnimationClip getAnimationClip() { + return this.animationClip; + } + + @Override + public boolean hasTransformFor(String joint) { + return this.nextAnimation.get().hasTransformFor(joint); + } + + @Override + public boolean isLinkAnimation() { + return true; + } + + @Override + public ConcurrentLinkAnimation get() { + return this; + } + + @Override + public ResourceLocation registryName() { + return null; + } + + @Override + public boolean isPresent() { + return true; + } + + @Override + public int id() { + return -1; + } + + @Override + public AnimationAccessor getAccessor() { + return this; + } + + @Override + public boolean inRegistry() { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/DynamicAnimation.java b/src/main/java/com/tiedup/remake/rig/anim/types/DynamicAnimation.java new file mode 100644 index 0000000..806fe00 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/DynamicAnimation.java @@ -0,0 +1,200 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import java.util.Map; +import java.util.Optional; + +import javax.annotation.Nullable; + +import com.mojang.blaze3d.vertex.PoseStack; + +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import com.tiedup.remake.rig.anim.AnimationClip; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.AnimationPlayer; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.TransformSheet; +import com.tiedup.remake.rig.anim.property.AnimationProperty; +import com.tiedup.remake.rig.anim.types.EntityState.StateFactor; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.anim.client.property.JointMaskEntry; +import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.main.EpicFightSharedConstants; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public abstract class DynamicAnimation { + protected final boolean isRepeat; + protected final float transitionTime; + protected AnimationClip animationClip; + + public DynamicAnimation() { + this(EpicFightSharedConstants.GENERAL_ANIMATION_TRANSITION_TIME, false); + } + + public DynamicAnimation(float transitionTime, boolean isRepeat) { + this.isRepeat = isRepeat; + this.transitionTime = transitionTime; + } + + public final Pose getRawPose(float time) { + return this.getAnimationClip().getPoseInTime(time); + } + + public Pose getPoseByTime(LivingEntityPatch entitypatch, float time, float partialTicks) { + Pose pose = this.getRawPose(time); + this.modifyPose(this, pose, entitypatch, time, partialTicks); + + return pose; + } + + /** Modify the pose both this and link animation. **/ + public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch entitypatch, float time, float partialTicks) { + } + + public void putOnPlayer(AnimationPlayer animationPlayer, LivingEntityPatch entitypatch) { + animationPlayer.setPlayAnimation(this.getAccessor()); + animationPlayer.tick(entitypatch); + animationPlayer.begin(this.getAccessor(), entitypatch); + } + + /** + * Called when the animation put on the {@link AnimationPlayer} + * @param entitypatch + */ + public void begin(LivingEntityPatch entitypatch) {} + + /** + * Called each tick when the animation is played + * @param entitypatch + */ + public void tick(LivingEntityPatch entitypatch) {} + + /** + * Called when both the animation finished or stopped by other animation. + * @param entitypatch + * @param nextAnimation the next animation to play after the animation ends + * @param isEnd whether the animation completed or not + * + * if @param isEnd true, nextAnimation is null + * if @param isEnd false, nextAnimation is not null + */ + public void end(LivingEntityPatch entitypatch, @Nullable AssetAccessor nextAnimation, boolean isEnd) {} + public void linkTick(LivingEntityPatch entitypatch, AssetAccessor linkAnimation) {}; + + public boolean hasTransformFor(String joint) { + return this.getTransfroms().containsKey(joint); + } + + @OnlyIn(Dist.CLIENT) + public Optional getJointMaskEntry(LivingEntityPatch entitypatch, boolean useCurrentMotion) { + return Optional.empty(); + } + + public EntityState getState(LivingEntityPatch entitypatch, float time) { + return EntityState.DEFAULT_STATE; + } + + public TypeFlexibleHashMap> getStatesMap(LivingEntityPatch entitypatch, float time) { + return new TypeFlexibleHashMap<> (false); + } + + public T getState(StateFactor stateFactor, LivingEntityPatch entitypatch, float time) { + return stateFactor.defaultValue(); + } + + public AnimationClip getAnimationClip() { + return this.animationClip; + } + + public Map getTransfroms() { + return this.getAnimationClip().getJointTransforms(); + } + + public float getPlaySpeed(LivingEntityPatch entitypatch, DynamicAnimation animation) { + return 1.0F; + } + + public TransformSheet getCoord() { + return this.getTransfroms().containsKey("Root") ? this.getTransfroms().get("Root") : TransformSheet.EMPTY_SHEET; + } + + public void setTotalTime(float totalTime) { + this.getAnimationClip().setClipTime(totalTime); + } + + public float getTotalTime() { + return this.getAnimationClip().getClipTime(); + } + + public float getTransitionTime() { + return this.transitionTime; + } + + public boolean isRepeat() { + return this.isRepeat; + } + + public boolean canBePlayedReverse() { + return false; + } + + public ResourceLocation getRegistryName() { + return EpicFightMod.identifier(""); + } + + public int getId() { + return -1; + } + + public Optional getProperty(AnimationProperty propertyType) { + return Optional.empty(); + } + + public boolean isBasicAttackAnimation() { + return false; + } + + public boolean isMainFrameAnimation() { + return false; + } + + public boolean isReboundAnimation() { + return false; + } + + public boolean isMetaAnimation() { + return false; + } + + public boolean isClientAnimation() { + return false; + } + + public boolean isStaticAnimation() { + return false; + } + + public abstract AnimationAccessor getAccessor(); + public abstract AssetAccessor getRealAnimation(); + + public boolean isLinkAnimation() { + return false; + } + + public boolean doesHeadRotFollowEntityHead() { + return false; + } + + @OnlyIn(Dist.CLIENT) + public void renderDebugging(PoseStack poseStack, MultiBufferSource buffer, LivingEntityPatch entitypatch, float playTime, float partialTicks) { + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/EntityState.java b/src/main/java/com/tiedup/remake/rig/anim/types/EntityState.java new file mode 100644 index 0000000..47c5f81 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/EntityState.java @@ -0,0 +1,146 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import java.util.function.Consumer; +import java.util.function.Function; + +import net.minecraft.world.damagesource.DamageSource; +import net.minecraftforge.event.entity.ProjectileImpactEvent; +import com.tiedup.remake.rig.util.AttackResult; +import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap; + +public class EntityState { + public static class StateFactor implements TypeFlexibleHashMap.TypeKey { + private final String name; + private final T defaultValue; + + public StateFactor(String name, T defaultValue) { + this.name = name; + this.defaultValue = defaultValue; + } + + public String toString() { + return this.name; + } + + public T defaultValue() { + return this.defaultValue; + } + } + + public static final EntityState DEFAULT_STATE = new EntityState(new TypeFlexibleHashMap<>(true)); + + public static final StateFactor TURNING_LOCKED = new StateFactor<>("turningLocked", false); + public static final StateFactor MOVEMENT_LOCKED = new StateFactor<>("movementLocked", false); + public static final StateFactor ATTACKING = new StateFactor<>("attacking", false); + public static final StateFactor CAN_BASIC_ATTACK = new StateFactor<>("canBasicAttack", true); + public static final StateFactor CAN_SKILL_EXECUTION = new StateFactor<>("canExecuteSkill", true); + public static final StateFactor CAN_USE_ITEM = new StateFactor<>("canUseItem", true); + public static final StateFactor CAN_SWITCH_HAND_ITEM = new StateFactor<>("canSwitchHandItem", true); + public static final StateFactor INACTION = new StateFactor<>("takingAction", false); + public static final StateFactor KNOCKDOWN = new StateFactor<>("knockdown", false); + public static final StateFactor LOCKON_ROTATE = new StateFactor<>("lockonRotate", false); + public static final StateFactor UPDATE_LIVING_MOTION = new StateFactor<>("updateLivingMotion", true); + public static final StateFactor HURT_LEVEL = new StateFactor<>("hurtLevel", 0); + public static final StateFactor PHASE_LEVEL = new StateFactor<>("phaseLevel", 0); + public static final StateFactor> ATTACK_RESULT = new StateFactor<>("attackResultModifier", (damagesource) -> AttackResult.ResultType.SUCCESS); + public static final StateFactor> PROJECTILE_IMPACT_RESULT = new StateFactor<>("projectileImpactResult", (event) -> {}); + + private final TypeFlexibleHashMap> stateMap; + + public EntityState(TypeFlexibleHashMap> states) { + this.stateMap = states; + } + + public void setState(StateFactor stateFactor, T val) { + this.stateMap.put(stateFactor, (Object)val); + } + + public T getState(StateFactor stateFactor) { + return this.stateMap.getOrDefault(stateFactor); + } + + public TypeFlexibleHashMap> getStateMap() { + return this.stateMap; + } + + public boolean turningLocked() { + return this.getState(EntityState.TURNING_LOCKED); + } + + public boolean movementLocked() { + return this.getState(EntityState.MOVEMENT_LOCKED); + } + + public boolean attacking() { + return this.getState(EntityState.ATTACKING); + } + + public AttackResult.ResultType attackResult(DamageSource damagesource) { + return this.getState(EntityState.ATTACK_RESULT).apply(damagesource); + } + + public void setProjectileImpactResult(ProjectileImpactEvent event) { + this.getState(EntityState.PROJECTILE_IMPACT_RESULT).accept(event); + } + + public boolean canBasicAttack() { + return this.getState(EntityState.CAN_BASIC_ATTACK); + } + + public boolean canUseSkill() { + return this.getState(EntityState.CAN_SKILL_EXECUTION); + } + + public boolean canUseItem() { + return this.canUseSkill() && this.getState(EntityState.CAN_USE_ITEM); + } + + public boolean canSwitchHoldingItem() { + return !this.inaction() && this.getState(EntityState.CAN_SWITCH_HAND_ITEM); + } + + public boolean inaction() { + return this.getState(EntityState.INACTION); + } + + public boolean updateLivingMotion() { + return this.getState(EntityState.UPDATE_LIVING_MOTION); + } + + public boolean hurt() { + return this.getState(EntityState.HURT_LEVEL) > 0; + } + + public int hurtLevel() { + return this.getState(EntityState.HURT_LEVEL); + } + + public boolean knockDown() { + return this.getState(EntityState.KNOCKDOWN); + } + + public boolean lockonRotate() { + return this.getState(EntityState.LOCKON_ROTATE); + } + + /** + * 1: anticipation + * 2: attacking + * 3: recovery + * @return level + */ + public int getLevel() { + return this.getState(EntityState.PHASE_LEVEL); + } + + @Override + public String toString() { + return this.stateMap.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/LayerOffAnimation.java b/src/main/java/com/tiedup/remake/rig/anim/types/LayerOffAnimation.java new file mode 100644 index 0000000..83211a7 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/LayerOffAnimation.java @@ -0,0 +1,124 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import java.util.Optional; + +import net.minecraft.client.Minecraft; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import com.tiedup.remake.rig.anim.AnimationClip; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.property.AnimationProperty; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.anim.client.Layer.Priority; +import com.tiedup.remake.rig.anim.client.property.JointMaskEntry; +import yesman.epicfight.gameasset.Animations; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +@OnlyIn(Dist.CLIENT) +public class LayerOffAnimation extends DynamicAnimation implements AnimationAccessor { + private AssetAccessor lastAnimation; + private Pose lastPose; + private final Priority layerPriority; + + public LayerOffAnimation(Priority layerPriority) { + this.layerPriority = layerPriority; + this.animationClip = new AnimationClip(); + } + + public void setLastPose(Pose pose) { + this.lastPose = pose; + } + + @Override + public void end(LivingEntityPatch entitypatch, AssetAccessor nextAnimation, boolean isEnd) { + if (entitypatch.isLogicalClient() && isEnd) { + entitypatch.getClientAnimator().baseLayer.disableLayer(this.layerPriority); + } + } + + @Override + public Pose getPoseByTime(LivingEntityPatch entitypatch, float time, float partialTicks) { + Pose lowerLayerPose = entitypatch.getClientAnimator().getComposedLayerPoseBelow(this.layerPriority, Minecraft.getInstance().getFrameTime()); + Pose interpolatedPose = Pose.interpolatePose(this.lastPose, lowerLayerPose, time / this.getTotalTime()); + interpolatedPose.disableJoint((joint) -> !this.lastPose.hasTransform(joint.getKey())); + + return interpolatedPose; + } + + @Override + public Optional getJointMaskEntry(LivingEntityPatch entitypatch, boolean useCurrentMotion) { + return this.lastAnimation.get().getJointMaskEntry(entitypatch, useCurrentMotion); + } + + @Override + public Optional getProperty(AnimationProperty propertyType) { + return this.lastAnimation.get().getProperty(propertyType); + } + + public void setLastAnimation(AssetAccessor animation) { + this.lastAnimation = animation; + } + + @Override + public boolean doesHeadRotFollowEntityHead() { + return this.lastAnimation.get().doesHeadRotFollowEntityHead(); + } + + @Override + public AssetAccessor getRealAnimation() { + return Animations.EMPTY_ANIMATION; + } + + @Override + public AnimationClip getAnimationClip() { + return this.animationClip; + } + + @Override + public boolean hasTransformFor(String joint) { + return this.lastPose.hasTransform(joint); + } + + @Override + public boolean isLinkAnimation() { + return true; + } + + @Override + public LayerOffAnimation get() { + return this; + } + + @Override + public ResourceLocation registryName() { + return null; + } + + @Override + public boolean isPresent() { + return true; + } + + @Override + public int id() { + return -1; + } + + @Override + public AnimationAccessor getAccessor() { + return this; + } + + @Override + public boolean inRegistry() { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/LinkAnimation.java b/src/main/java/com/tiedup/remake/rig/anim/types/LinkAnimation.java new file mode 100644 index 0000000..3650a7a --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/LinkAnimation.java @@ -0,0 +1,244 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import java.util.Map; +import java.util.Optional; + +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import com.tiedup.remake.rig.anim.AnimationClip; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.anim.Keyframe; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.TransformSheet; +import com.tiedup.remake.rig.anim.types.EntityState.StateFactor; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.anim.client.property.JointMaskEntry; +import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class LinkAnimation extends DynamicAnimation implements AnimationAccessor { + protected TransformSheet coord; + protected AssetAccessor fromAnimation; + protected AssetAccessor toAnimation; + protected float nextStartTime; + + public LinkAnimation() { + this.animationClip = new AnimationClip(); + } + + @Override + public void tick(LivingEntityPatch entitypatch) { + this.toAnimation.get().linkTick(entitypatch, this); + } + + @Override + public void end(LivingEntityPatch entitypatch, AssetAccessor nextAnimation, boolean isEnd) { + if (!isEnd) { + this.toAnimation.get().end(entitypatch, nextAnimation, isEnd); + } else { + if (this.nextStartTime > 0.0F) { + entitypatch.getAnimator().getPlayer(this).ifPresent(player -> { + player.setElapsedTime(this.nextStartTime); + player.markDoNotResetTime(); + }); + } + } + } + + @Override + public TypeFlexibleHashMap> getStatesMap(LivingEntityPatch entitypatch, float time) { + float timeInRealAnimation = Math.max(time - (this.getTotalTime() - this.nextStartTime), 0.0F); + TypeFlexibleHashMap> map = this.toAnimation.get().getStatesMap(entitypatch, timeInRealAnimation); + + for (Map.Entry, Object> entry : map.entrySet()) { + Object val = this.toAnimation.get().getModifiedLinkState(entry.getKey(), entry.getValue(), entitypatch, time); + map.put(entry.getKey(), val); + } + + return map; + } + + @Override + public EntityState getState(LivingEntityPatch entitypatch, float time) { + float timeInRealAnimation = Math.max(time - (this.getTotalTime() - this.nextStartTime), 0.0F); + + EntityState state = this.toAnimation.get().getState(entitypatch, timeInRealAnimation); + TypeFlexibleHashMap> map = state.getStateMap(); + + for (Map.Entry, Object> entry : map.entrySet()) { + Object val = this.toAnimation.get().getModifiedLinkState(entry.getKey(), entry.getValue(), entitypatch, time); + map.put(entry.getKey(), val); + } + + return state; + } + + @SuppressWarnings("unchecked") + @Override + public T getState(StateFactor stateFactor, LivingEntityPatch entitypatch, float time) { + float timeInRealAnimation = Math.max(time - (this.getTotalTime() - this.nextStartTime), 0.0F); + T state = this.toAnimation.get().getState(stateFactor, entitypatch, timeInRealAnimation); + + return (T)this.toAnimation.get().getModifiedLinkState(stateFactor, state, entitypatch, time); + } + + @Override + public Pose getPoseByTime(LivingEntityPatch entitypatch, float time, float partialTicks) { + Pose nextStartingPose = this.toAnimation.get().getPoseByTime(entitypatch, this.nextStartTime, partialTicks); + + /** + * Update dest pose + */ + for (Map.Entry entry : nextStartingPose.getJointTransformData().entrySet()) { + if (this.animationClip.hasJointTransform(entry.getKey())) { + Keyframe[] keyframe = this.animationClip.getJointTransform(entry.getKey()).getKeyframes(); + JointTransform jt = keyframe[keyframe.length - 1].transform(); + JointTransform newJt = nextStartingPose.getJointTransformData().get(entry.getKey()); + newJt.translation().set(jt.translation()); + jt.copyFrom(newJt); + } + } + + return super.getPoseByTime(entitypatch, time, partialTicks); + } + + @Override + public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch entitypatch, float time, float partialTicks) { + // Bad implementation: Add root joint as coord in loading animation + if (this.toAnimation.get() instanceof ActionAnimation actionAnimation) { + if (!this.getTransfroms().containsKey("Coord")) { + actionAnimation.correctRootJoint(this, pose, entitypatch, time, partialTicks); + } + } + } + + @Override + public float getPlaySpeed(LivingEntityPatch entitypatch, DynamicAnimation animation) { + return this.toAnimation.get().getPlaySpeed(entitypatch, animation); + } + + public void setConnectedAnimations(AssetAccessor from, AssetAccessor to) { + this.fromAnimation = from.get().getRealAnimation(); + this.toAnimation = to; + } + + public AssetAccessor getNextAnimation() { + return this.toAnimation; + } + + @Override + public TransformSheet getCoord() { + if (this.coord != null) { + return this.coord; + } else if (this.getTransfroms().containsKey("Root")) { + return this.getTransfroms().get("Root"); + } + + return TransformSheet.EMPTY_SHEET; + } + + @OnlyIn(Dist.CLIENT) + public Optional getJointMaskEntry(LivingEntityPatch entitypatch, boolean useCurrentMotion) { + return useCurrentMotion ? this.toAnimation.get().getJointMaskEntry(entitypatch, true) : this.fromAnimation.get().getJointMaskEntry(entitypatch, false); + } + + @Override + public boolean isMainFrameAnimation() { + return this.toAnimation.get().isMainFrameAnimation(); + } + + @Override + public boolean isReboundAnimation() { + return this.toAnimation.get().isReboundAnimation(); + } + + @Override + public boolean doesHeadRotFollowEntityHead() { + return this.fromAnimation.get().doesHeadRotFollowEntityHead() && this.toAnimation.get().doesHeadRotFollowEntityHead(); + } + + @Override + public AssetAccessor getRealAnimation() { + return this.toAnimation; + } + + public AssetAccessor getFromAnimation() { + return this.fromAnimation; + } + + @Override + public AnimationAccessor getAccessor() { + return this; + } + + public void copyTo(LinkAnimation dest) { + dest.setConnectedAnimations(this.fromAnimation, this.toAnimation); + dest.setTotalTime(this.getTotalTime()); + dest.getAnimationClip().reset(); + this.getTransfroms().forEach((jointName, transformSheet) -> dest.getAnimationClip().addJointTransform(jointName, transformSheet.copyAll())); + } + + public void loadCoord(TransformSheet coord) { + this.coord = coord; + } + + public float getNextStartTime() { + return this.nextStartTime; + } + + public void setNextStartTime(float nextStartTime) { + this.nextStartTime = nextStartTime; + } + + public void resetNextStartTime() { + this.nextStartTime = 0.0F; + } + + @Override + public boolean isLinkAnimation() { + return true; + } + + @Override + public String toString() { + return "From " + this.fromAnimation + " to " + this.toAnimation; + } + + @Override + public AnimationClip getAnimationClip() { + return this.animationClip; + } + + @Override + public LinkAnimation get() { + return this; + } + + @Override + public ResourceLocation registryName() { + return null; + } + + @Override + public boolean isPresent() { + return true; + } + + @Override + public int id() { + return -1; + } + + @Override + public boolean inRegistry() { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/MainFrameAnimation.java b/src/main/java/com/tiedup/remake/rig/anim/types/MainFrameAnimation.java new file mode 100644 index 0000000..4dbde36 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/MainFrameAnimation.java @@ -0,0 +1,93 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty; +import com.tiedup.remake.rig.anim.types.EntityState.StateFactor; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.anim.client.Layer; +import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties; +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap; +import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.patch.PlayerPatch; +import com.tiedup.remake.rig.patch.ServerPlayerPatch; +import yesman.epicfight.world.entity.eventlistener.ActionEvent; +import yesman.epicfight.world.entity.eventlistener.PlayerEventListener.EventType; + +public class MainFrameAnimation extends StaticAnimation { + public MainFrameAnimation(float convertTime, AnimationAccessor accessor, AssetAccessor armature) { + super(convertTime, false, accessor, armature); + } + + public MainFrameAnimation(float convertTime, String path, AssetAccessor armature) { + super(convertTime, false, path, armature); + } + + @Override + public void begin(LivingEntityPatch entitypatch) { + if (entitypatch.getAnimator().getPlayerFor(null).getAnimation().get() == this) { + TypeFlexibleHashMap> stateMap = this.stateSpectrum.getStateMap(entitypatch, 0.0F); + TypeFlexibleHashMap> modifiedStateMap = new TypeFlexibleHashMap<> (false); + stateMap.forEach((k, v) -> modifiedStateMap.put(k, this.getModifiedLinkState(k, v, entitypatch, 0.0F))); + entitypatch.updateEntityState(new EntityState(modifiedStateMap)); + } + + if (entitypatch.isLogicalClient()) { + entitypatch.updateMotion(false); + + this.getProperty(StaticAnimationProperty.RESET_LIVING_MOTION).ifPresentOrElse(livingMotion -> { + entitypatch.getClientAnimator().forceResetBeforeAction(livingMotion, livingMotion); + }, () -> { + entitypatch.getClientAnimator().resetMotion(true); + entitypatch.getClientAnimator().resetCompositeMotion(); + }); + + entitypatch.getClientAnimator().getPlayerFor(this.getAccessor()).setReversed(false); + } + + super.begin(entitypatch); + + if (entitypatch instanceof PlayerPatch playerpatch) { + if (playerpatch.isLogicalClient()) { + if (playerpatch.getOriginal().isLocalPlayer()) { + playerpatch.getEventListener().triggerEvents(EventType.ACTION_EVENT_CLIENT, new ActionEvent<>(playerpatch, this.getAccessor())); + } + } else { + ActionEvent actionEvent = new ActionEvent<>(playerpatch, this.getAccessor()); + playerpatch.getEventListener().triggerEvents(EventType.ACTION_EVENT_SERVER, actionEvent); + + if (actionEvent.shouldResetActionTick()) { + playerpatch.resetActionTick(); + } + } + } + } + + @Override + public void tick(LivingEntityPatch entitypatch) { + super.tick(entitypatch); + + if (entitypatch.getEntityState().movementLocked()) { + entitypatch.getOriginal().walkAnimation.setSpeed(0); + } + } + + @Override + public boolean isMainFrameAnimation() { + return true; + } + + @Override + @OnlyIn(Dist.CLIENT) + public Layer.Priority getPriority() { + return this.getProperty(ClientAnimationProperties.PRIORITY).orElse(Layer.Priority.HIGHEST); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/MovementAnimation.java b/src/main/java/com/tiedup/remake/rig/anim/types/MovementAnimation.java new file mode 100644 index 0000000..b3dc92c --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/MovementAnimation.java @@ -0,0 +1,50 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.armature.Armature; +import yesman.epicfight.main.EpicFightSharedConstants; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class MovementAnimation extends StaticAnimation { + public MovementAnimation(boolean isRepeat, AnimationAccessor accessor, AssetAccessor armature) { + super(EpicFightSharedConstants.GENERAL_ANIMATION_TRANSITION_TIME, isRepeat, accessor, armature); + } + + public MovementAnimation(float transitionTime, boolean isRepeat, AnimationAccessor accessor, AssetAccessor armature) { + super(transitionTime, isRepeat, accessor, armature); + } + + /** + * For datapack animations + */ + public MovementAnimation(float transitionTime, boolean isRepeat, String path, AssetAccessor armature) { + super(transitionTime, isRepeat, path, armature); + } + + @Override + public float getPlaySpeed(LivingEntityPatch entitypatch, DynamicAnimation animation) { + if (animation.isLinkAnimation()) { + return 1.0F; + } + + float movementSpeed = 1.0F; + + if (Math.abs(entitypatch.getOriginal().walkAnimation.speed() - entitypatch.getOriginal().walkAnimation.speed(1)) < 0.007F) { + movementSpeed *= (entitypatch.getOriginal().walkAnimation.speed() * 1.16F); + } + + return movementSpeed; + } + + @Override + public boolean canBePlayedReverse() { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/anim/types/StaticAnimation.java b/src/main/java/com/tiedup/remake/rig/anim/types/StaticAnimation.java new file mode 100644 index 0000000..a3b13b1 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/anim/types/StaticAnimation.java @@ -0,0 +1,646 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim.types; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +import com.google.common.collect.Maps; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; + +import io.netty.util.internal.StringUtil; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import com.tiedup.remake.rig.anim.AnimationClip; +import com.tiedup.remake.rig.anim.AnimationManager; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.AnimationVariables; +import com.tiedup.remake.rig.anim.AnimationVariables.IndependentAnimationVariableKey; +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.anim.Keyframe; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.TransformSheet; +import com.tiedup.remake.rig.anim.property.AnimationEvent; +import com.tiedup.remake.rig.anim.property.AnimationEvent.SimpleEvent; +import com.tiedup.remake.rig.anim.property.AnimationParameters; +import com.tiedup.remake.rig.anim.property.AnimationProperty; +import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty; +import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier; +import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty; +import com.tiedup.remake.rig.anim.types.EntityState.StateFactor; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.asset.JsonAssetLoader; +import com.tiedup.remake.rig.anim.client.Layer; +import com.tiedup.remake.rig.anim.client.Layer.LayerType; +import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties; +import com.tiedup.remake.rig.anim.client.property.JointMaskEntry; +import com.tiedup.remake.rig.anim.client.property.TrailInfo; +import com.tiedup.remake.rig.exception.AssetLoadingException; +import com.tiedup.remake.rig.armature.Armature; +import yesman.epicfight.api.physics.ik.InverseKinematicsProvider; +import yesman.epicfight.api.physics.ik.InverseKinematicsSimulatable; +import yesman.epicfight.api.physics.ik.InverseKinematicsSimulator; +import yesman.epicfight.api.physics.ik.InverseKinematicsSimulator.BakedInverseKinematicsDefinition; +import yesman.epicfight.api.physics.ik.InverseKinematicsSimulator.InverseKinematicsObject; +import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; +import yesman.epicfight.client.ClientEngine; +import com.tiedup.remake.rig.render.TiedUpRenderTypes; +import yesman.epicfight.client.renderer.RenderingTool; +import com.tiedup.remake.rig.render.item.RenderItemBase; +import yesman.epicfight.gameasset.Animations; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.main.EpicFightSharedConstants; +import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.patch.PlayerPatch; +import yesman.epicfight.world.entity.eventlistener.AnimationBeginEvent; +import yesman.epicfight.world.entity.eventlistener.AnimationEndEvent; +import yesman.epicfight.world.entity.eventlistener.PlayerEventListener.EventType; + +public class StaticAnimation extends DynamicAnimation implements InverseKinematicsProvider { + public static final IndependentAnimationVariableKey HAD_NO_PHYSICS = AnimationVariables.independent((animator) -> false, true); + + public static String getFileHash(ResourceLocation rl) { + String fileHash; + + try { + JsonAssetLoader jsonfile = new JsonAssetLoader(AnimationManager.getAnimationResourceManager(), rl); + fileHash = jsonfile.getFileHash(); + } catch (AssetLoadingException e) { + fileHash = StringUtil.EMPTY_STRING; + } + + return fileHash; + } + + protected final Map, Object> properties = Maps.newHashMap(); + + /** + * States will bind into animation on {@link AnimationManager#apply} + */ + protected final StateSpectrum.Blueprint stateSpectrumBlueprint = new StateSpectrum.Blueprint(); + protected final StateSpectrum stateSpectrum = new StateSpectrum(); + protected final AssetAccessor armature; + protected ResourceLocation resourceLocation; + protected AnimationAccessor accessor; + private final String filehash; + + public StaticAnimation() { + super(0.0F, true); + + this.resourceLocation = EpicFightMod.identifier("emtpy"); + this.armature = null; + this.filehash = StringUtil.EMPTY_STRING; + } + + public StaticAnimation(boolean isRepeat, AnimationAccessor accessor, AssetAccessor armature) { + this(EpicFightSharedConstants.GENERAL_ANIMATION_TRANSITION_TIME, isRepeat, accessor, armature); + } + + public StaticAnimation(float transitionTime, boolean isRepeat, AnimationAccessor accessor, AssetAccessor armature) { + super(transitionTime, isRepeat); + + this.resourceLocation = ResourceLocation.fromNamespaceAndPath(accessor.registryName().getNamespace(), "animmodels/animations/" + accessor.registryName().getPath() + ".json"); + + this.armature = armature; + this.accessor = accessor; + this.filehash = getFileHash(this.resourceLocation); + } + + /* Resourcepack animations */ + public StaticAnimation(float transitionTime, boolean isRepeat, String path, AssetAccessor armature) { + super(transitionTime, isRepeat); + + ResourceLocation registryName = ResourceLocation.parse(path); + this.resourceLocation = ResourceLocation.fromNamespaceAndPath(registryName.getNamespace(), "animmodels/animations/" + registryName.getPath() + ".json"); + this.armature = armature; + this.filehash = StringUtil.EMPTY_STRING; + } + + /* Multilayer Constructor */ + public StaticAnimation(ResourceLocation fileLocation, float transitionTime, boolean isRepeat, String registryName, AssetAccessor armature) { + super(transitionTime, isRepeat); + + this.resourceLocation = fileLocation; + this.armature = armature; + this.filehash = StringUtil.EMPTY_STRING; + } + + public void loadAnimation() { + if (!this.isMetaAnimation()) { + if (this.properties.containsKey(StaticAnimationProperty.IK_DEFINITION)) { + this.animationClip = AnimationManager.getInstance().loadAnimationClip(this, JsonAssetLoader::loadAllJointsClipForAnimation); + + this.getProperty(StaticAnimationProperty.IK_DEFINITION).ifPresent(ikDefinitions -> { + boolean correctY = this.getProperty(ActionAnimationProperty.MOVE_VERTICAL).orElse(false); + boolean correctZ = this.isMainFrameAnimation(); + + List bakedIKDefinitionList = ikDefinitions.stream().map(ikDefinition -> ikDefinition.bake(this.armature, this.animationClip.getJointTransforms(), correctY, correctZ)).toList(); + this.addProperty(StaticAnimationProperty.BAKED_IK_DEFINITION, bakedIKDefinitionList); + + // Remove the unbaked data + this.properties.remove(StaticAnimationProperty.IK_DEFINITION); + }); + } else { + this.animationClip = AnimationManager.getInstance().loadAnimationClip(this, JsonAssetLoader::loadClipForAnimation); + } + + this.animationClip.bakeKeyframes(); + } + } + + public void postInit() { + this.stateSpectrum.readFrom(this.stateSpectrumBlueprint); + } + + @Override + public AnimationClip getAnimationClip() { + if (this.animationClip == null) { + this.loadAnimation(); + } + + return this.animationClip; + } + + public void setLinkAnimation(final AssetAccessor fromAnimation, Pose startPose, boolean isOnSameLayer, float transitionTimeModifier, LivingEntityPatch entitypatch, LinkAnimation dest) { + if (!entitypatch.isLogicalClient()) { + startPose = Animations.EMPTY_ANIMATION.getPoseByTime(entitypatch, 0.0F, 1.0F); + } + + dest.resetNextStartTime(); + + float playTime = this.getPlaySpeed(entitypatch, dest); + PlaybackSpeedModifier playSpeedModifier = this.getRealAnimation().get().getProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER).orElse(null); + + if (playSpeedModifier != null) { + playTime = playSpeedModifier.modify(dest, entitypatch, playTime, 0.0F, playTime); + } + + playTime = Math.abs(playTime); + playTime *= EpicFightSharedConstants.A_TICK; + + float linkTime = transitionTimeModifier > 0.0F ? transitionTimeModifier + this.transitionTime : this.transitionTime; + float totalTime = playTime * (int)Math.ceil(linkTime / playTime); + float nextStartTime = Math.max(0.0F, -transitionTimeModifier); + nextStartTime += totalTime - linkTime; + + dest.setNextStartTime(nextStartTime); + dest.getAnimationClip().reset(); + dest.setTotalTime(totalTime); + dest.setConnectedAnimations(fromAnimation, this.getAccessor()); + + Map data1 = startPose.getJointTransformData(); + Map data2 = this.getPoseByTime(entitypatch, nextStartTime, 0.0F).getJointTransformData(); + Set joint1 = new HashSet<> (isOnSameLayer ? data1.keySet() : Set.of()); + Set joint2 = new HashSet<> (data2.keySet()); + + if (entitypatch.isLogicalClient()) { + JointMaskEntry entry = fromAnimation.get().getJointMaskEntry(entitypatch, false).orElse(null); + JointMaskEntry entry2 = this.getJointMaskEntry(entitypatch, true).orElse(null); + + if (entry != null) { + joint1.removeIf((jointName) -> entry.isMasked(fromAnimation.get().getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ? + entitypatch.getClientAnimator().currentMotion() : entitypatch.getClientAnimator().currentCompositeMotion(), jointName)); + } + + if (entry2 != null) { + joint2.removeIf((jointName) -> entry2.isMasked(this.getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ? + entitypatch.getCurrentLivingMotion() : entitypatch.currentCompositeMotion, jointName)); + } + } + + joint1.addAll(joint2); + + if (linkTime != totalTime) { + Map firstPose = this.getPoseByTime(entitypatch, 0.0F, 0.0F).getJointTransformData(); + + for (String jointName : joint1) { + Keyframe[] keyframes = new Keyframe[3]; + keyframes[0] = new Keyframe(0.0F, data1.get(jointName)); + keyframes[1] = new Keyframe(linkTime, firstPose.get(jointName)); + keyframes[2] = new Keyframe(totalTime, data2.get(jointName)); + TransformSheet sheet = new TransformSheet(keyframes); + dest.getAnimationClip().addJointTransform(jointName, sheet); + } + } else { + for (String jointName : joint1) { + Keyframe[] keyframes = new Keyframe[2]; + keyframes[0] = new Keyframe(0.0F, data1.get(jointName)); + keyframes[1] = new Keyframe(totalTime, data2.get(jointName)); + TransformSheet sheet = new TransformSheet(keyframes); + dest.getAnimationClip().addJointTransform(jointName, sheet); + } + } + } + + @Override + public void begin(LivingEntityPatch entitypatch) { + // Load if null + this.getAnimationClip(); + + // Please fix this implementation when minecraft supports any mixinable method that returns noPhysics variable + this.getProperty(StaticAnimationProperty.NO_PHYSICS).ifPresent(val -> { + if (val) { + entitypatch.getAnimator().getVariables().put(HAD_NO_PHYSICS, this.getAccessor(), entitypatch.getOriginal().noPhysics); + entitypatch.getOriginal().noPhysics = true; + } + }); + + if (entitypatch.isLogicalClient()) { + this.getProperty(ClientAnimationProperties.TRAIL_EFFECT).ifPresent(trailInfos -> { + int idx = 0; + + for (TrailInfo trailInfo : trailInfos) { + double eid = Double.longBitsToDouble((long)entitypatch.getOriginal().getId()); + double animid = Double.longBitsToDouble((long)this.getId()); + double jointId = Double.longBitsToDouble((long)this.armature.get().searchJointByName(trailInfo.joint()).getId()); + double index = Double.longBitsToDouble((long)idx++); + + if (trailInfo.hand() != null) { + RenderItemBase renderitembase = ClientEngine.getInstance().renderEngine.getItemRenderer(entitypatch.getAdvancedHoldingItemStack(trailInfo.hand())); + + if (renderitembase != null && renderitembase.trailInfo() != null) { + trailInfo = renderitembase.trailInfo().overwrite(trailInfo); + } + } + + if (!trailInfo.playable()) { + continue; + } + + entitypatch.getOriginal().level().addParticle(trailInfo.particle(), eid, 0, animid, jointId, index, 0); + } + }); + } + + this.getProperty(StaticAnimationProperty.ON_BEGIN_EVENTS).ifPresent(events -> { + for (SimpleEvent event : events) { + event.execute(entitypatch, this.getAccessor(), 0.0F, 0.0F); + } + }); + + if (entitypatch instanceof PlayerPatch playerpatch) { + playerpatch.getEventListener().triggerEvents(EventType.ANIMATION_BEGIN_EVENT, new AnimationBeginEvent(playerpatch, this)); + } + } + + @Override + public void end(LivingEntityPatch entitypatch, AssetAccessor nextAnimation, boolean isEnd) { + if (entitypatch instanceof PlayerPatch playerpatch) { + playerpatch.getEventListener().triggerEvents(EventType.ANIMATION_END_EVENT, new AnimationEndEvent(playerpatch, this, isEnd)); + } + + this.getProperty(StaticAnimationProperty.ON_END_EVENTS).ifPresent((events) -> { + for (SimpleEvent event : events) { + event.executeWithNewParams(entitypatch, this.getAccessor(), this.getTotalTime(), this.getTotalTime(), event.getParameters() == null ? AnimationParameters.of(isEnd) : AnimationParameters.addParameter(event.getParameters(), isEnd)); + } + }); + + this.getProperty(StaticAnimationProperty.NO_PHYSICS).ifPresent((val) -> { + if (val) { + entitypatch.getOriginal().noPhysics = entitypatch.getAnimator().getVariables().getOrDefault(HAD_NO_PHYSICS, this.getAccessor()); + } + }); + + entitypatch.getAnimator().getVariables().removeAll(this.getAccessor()); + } + + @Override + public void tick(LivingEntityPatch entitypatch) { + this.getProperty(StaticAnimationProperty.NO_PHYSICS).ifPresent((val) -> { + if (val) { + entitypatch.getOriginal().noPhysics = true; + } + }); + + this.getProperty(StaticAnimationProperty.TICK_EVENTS).ifPresent((events) -> { + entitypatch.getAnimator().getPlayer(this.getAccessor()).ifPresent(player -> { + for (AnimationEvent event : events) { + float prevElapsed = player.getPrevElapsedTime(); + float elapsed = player.getElapsedTime(); + + event.execute(entitypatch, this.getAccessor(), prevElapsed, elapsed); + } + }); + }); + } + + @Override + public EntityState getState(LivingEntityPatch entitypatch, float time) { + return new EntityState(this.getStatesMap(entitypatch, time)); + } + + @Override + public TypeFlexibleHashMap> getStatesMap(LivingEntityPatch entitypatch, float time) { + return this.stateSpectrum.getStateMap(entitypatch, time); + } + + @Override + public T getState(StateFactor stateFactor, LivingEntityPatch entitypatch, float time) { + return this.stateSpectrum.getSingleState(stateFactor, entitypatch, time); + } + + @Override + public Optional getJointMaskEntry(LivingEntityPatch entitypatch, boolean useCurrentMotion) { + return this.getProperty(ClientAnimationProperties.JOINT_MASK); + } + + @Override + public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch entitypatch, float time, float partialTicks) { + entitypatch.poseTick(animation, pose, time, partialTicks); + + this.getProperty(StaticAnimationProperty.POSE_MODIFIER).ifPresent((poseModifier) -> { + poseModifier.modify(animation, pose, entitypatch, time, partialTicks); + }); + } + + @Override + public boolean isStaticAnimation() { + return true; + } + + @Override + public boolean doesHeadRotFollowEntityHead() { + return !this.getProperty(StaticAnimationProperty.FIXED_HEAD_ROTATION).orElse(false); + } + + @Override + public int getId() { + return this.accessor.id(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StaticAnimation staticAnimation) { + if (this.accessor != null && staticAnimation.accessor != null) { + return this.getId() == staticAnimation.getId(); + } + } + + return super.equals(obj); + } + + public boolean idBetween(StaticAnimation a1, StaticAnimation a2) { + return a1.getId() <= this.getId() && a2.getId() >= this.getId(); + } + + public boolean in(StaticAnimation[] animations) { + for (StaticAnimation animation : animations) { + if (this.equals(animation)) { + return true; + } + } + + return false; + } + + public boolean in(AnimationAccessor[] animationProviders) { + for (AnimationAccessor animationProvider : animationProviders) { + if (this.equals(animationProvider.get())) { + return true; + } + } + + return false; + } + + @SuppressWarnings("unchecked") + public A setResourceLocation(String namespace, String path) { + this.resourceLocation = ResourceLocation.fromNamespaceAndPath(namespace, "animmodels/animations/" + path + ".json"); + return (A)this; + } + + public ResourceLocation getLocation() { + return this.resourceLocation; + } + + @Override + public ResourceLocation getRegistryName() { + return this.accessor.registryName(); + } + + public AssetAccessor getArmature() { + return this.armature; + } + + public String getFileHash() { + return this.filehash; + } + + @Override + public float getPlaySpeed(LivingEntityPatch entitypatch, DynamicAnimation animation) { + return 1.0F; + } + + @Override + public TransformSheet getCoord() { + return this.getProperty(ActionAnimationProperty.COORD).orElse(super.getCoord()); + } + + @Override + public String toString() { + String classPath = this.getClass().toString(); + return classPath.substring(classPath.lastIndexOf(".") + 1) + " " + this.getLocation(); + } + + /** + * Internal use only + */ + @Deprecated + public StaticAnimation addPropertyUnsafe(AnimationProperty propertyType, Object value) { + this.properties.put(propertyType, value); + this.getSubAnimations().forEach((subAnimation) -> subAnimation.get().addPropertyUnsafe(propertyType, value)); + return this; + } + + @SuppressWarnings("unchecked") + public A addProperty(StaticAnimationProperty propertyType, V value) { + this.properties.put(propertyType, value); + this.getSubAnimations().forEach((subAnimation) -> subAnimation.get().addProperty(propertyType, value)); + return (A)this; + } + + @SuppressWarnings("unchecked") + public A removeProperty(StaticAnimationProperty propertyType) { + this.properties.remove(propertyType); + this.getSubAnimations().forEach((subAnimation) -> subAnimation.get().removeProperty(propertyType)); + return (A)this; + } + + @SafeVarargs + @SuppressWarnings("unchecked") + public final A addEvents(StaticAnimationProperty key, AnimationEvent... events) { + this.properties.computeIfPresent(key, (k, v) -> { + return Stream.concat(((Collection)v).stream(), List.of(events).stream()).toList(); + }); + + this.properties.computeIfAbsent(key, (k) -> { + return List.of(events); + }); + + this.getSubAnimations().forEach((subAnimation) -> subAnimation.get().addEvents(key, events)); + + return (A)this; + } + + @SuppressWarnings("unchecked") + public A addEvents(AnimationEvent... events) { + this.properties.computeIfPresent(StaticAnimationProperty.TICK_EVENTS, (k, v) -> { + return Stream.concat(((Collection)v).stream(), List.of(events).stream()).toList(); + }); + + this.properties.computeIfAbsent(StaticAnimationProperty.TICK_EVENTS, (k) -> { + return List.of(events); + }); + + this.getSubAnimations().forEach((subAnimation) -> subAnimation.get().addEvents(events)); + + return (A)this; + } + + @SuppressWarnings("unchecked") + @Override + public Optional getProperty(AnimationProperty propertyType) { + return (Optional) Optional.ofNullable(this.properties.get(propertyType)); + } + + @OnlyIn(Dist.CLIENT) + public Layer.Priority getPriority() { + return this.getProperty(ClientAnimationProperties.PRIORITY).orElse(Layer.Priority.LOWEST); + } + + @OnlyIn(Dist.CLIENT) + public Layer.LayerType getLayerType() { + return this.getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(LayerType.BASE_LAYER); + } + + @SuppressWarnings("unchecked") + public A newTimePair(float start, float end) { + this.stateSpectrumBlueprint.newTimePair(start, end); + return (A)this; + } + + @SuppressWarnings("unchecked") + public A newConditionalTimePair(Function, Integer> condition, float start, float end) { + this.stateSpectrumBlueprint.newConditionalTimePair(condition, start, end); + return (A)this; + } + + @SuppressWarnings("unchecked") + public A addState(StateFactor factor, T val) { + this.stateSpectrumBlueprint.addState(factor, val); + return (A)this; + } + + @SuppressWarnings("unchecked") + public A removeState(StateFactor factor) { + this.stateSpectrumBlueprint.removeState(factor); + return (A)this; + } + + @SuppressWarnings("unchecked") + public A addConditionalState(int metadata, StateFactor factor, T val) { + this.stateSpectrumBlueprint.addConditionalState(metadata, factor, val); + return (A)this; + } + + @SuppressWarnings("unchecked") + public A addStateRemoveOld(StateFactor factor, T val) { + this.stateSpectrumBlueprint.addStateRemoveOld(factor, val); + return (A)this; + } + + @SuppressWarnings("unchecked") + public A addStateIfNotExist(StateFactor factor, T val) { + this.stateSpectrumBlueprint.addStateIfNotExist(factor, val); + return (A)this; + } + + public Object getModifiedLinkState(StateFactor factor, Object val, LivingEntityPatch entitypatch, float elapsedTime) { + return val; + } + + public List> getSubAnimations() { + return List.of(); + } + + @Override + public AnimationAccessor getRealAnimation() { + return this.getAccessor(); + } + + @SuppressWarnings("unchecked") + @Override + public AnimationAccessor getAccessor() { + return (AnimationAccessor)this.accessor; + } + + public void setAccessor(AnimationAccessor accessor) { + this.accessor = accessor; + } + + public void invalidate() { + this.accessor = null; + } + + public boolean isInvalid() { + return this.accessor == null; + } + + @OnlyIn(Dist.CLIENT) + public void renderDebugging(PoseStack poseStack, MultiBufferSource buffer, LivingEntityPatch entitypatch, float playTime, float partialTicks) { + if (entitypatch instanceof InverseKinematicsSimulatable ikSimulatable) { + this.getProperty(StaticAnimationProperty.BAKED_IK_DEFINITION).ifPresent((ikDefinitions) -> { + OpenMatrix4f modelmat = ikSimulatable.getModelMatrix(partialTicks); + LivingEntity originalEntity = entitypatch.getOriginal(); + Vec3 entitypos = originalEntity.position(); + float x = (float)entitypos.x; + float y = (float)entitypos.y; + float z = (float)entitypos.z; + float xo = (float)originalEntity.xo; + float yo = (float)originalEntity.yo; + float zo = (float)originalEntity.zo; + OpenMatrix4f toModelPos = OpenMatrix4f.mul(OpenMatrix4f.createTranslation(xo + (x - xo) * partialTicks, yo + (y - yo) * partialTicks, zo + (z - zo) * partialTicks), modelmat, null).invert(); + + for (BakedInverseKinematicsDefinition bakedIKInfo : this.getProperty(StaticAnimationProperty.BAKED_IK_DEFINITION).orElse(null)) { + ikSimulatable.getIKSimulator().getRunningObject(bakedIKInfo.endJoint()).ifPresent((ikObjet) -> { + VertexConsumer vertexBuilder = buffer.getBuffer(EpicFightRenderTypes.debugQuads()); + Vec3f worldtargetpos = ikObjet.getDestination(); + Vec3f modeltargetpos = OpenMatrix4f.transform3v(toModelPos, worldtargetpos, null).multiply(-1.0F, 1.0F, -1.0F); + RenderingTool.drawQuad(poseStack, vertexBuilder, modeltargetpos, 0.5F, 1.0F, 0.0F, 0.0F); + Vec3f jointWorldPos = ikObjet.getTipPosition(partialTicks); + Vec3f jointModelpos = OpenMatrix4f.transform3v(toModelPos, jointWorldPos, null); + RenderingTool.drawQuad(poseStack, vertexBuilder, jointModelpos.multiply(-1.0F, 1.0F, -1.0F), 0.4F, 0.0F, 0.0F, 1.0F); + + Pose pose = new Pose(); + + for (String jointName : this.getTransfroms().keySet()) { + pose.putJointData(jointName, this.getTransfroms().get(jointName).getInterpolatedTransform(playTime)); + } + }); + } + }); + } + } + + @Override + public InverseKinematicsObject createSimulationData(InverseKinematicsProvider provider, InverseKinematicsSimulatable simOwner, InverseKinematicsSimulator.InverseKinematicsBuilder simBuilder) { + return new InverseKinematicsObject(simBuilder); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/armature/Armature.java b/src/main/java/com/tiedup/remake/rig/armature/Armature.java new file mode 100644 index 0000000..f988e40 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/armature/Armature.java @@ -0,0 +1,270 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.armature; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.Map; +import java.util.NoSuchElementException; + +import com.google.common.collect.Maps; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import com.tiedup.remake.rig.armature.Joint; +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.asset.JsonAssetLoader; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.main.EpicFightSharedConstants; + +public class Armature { + private final String name; + private final Int2ObjectMap jointById; + private final Map jointByName; + private final Map pathIndexMap; + private final int jointCount; + private final OpenMatrix4f[] poseMatrices; + public final Joint rootJoint; + + public Armature(String name, int jointNumber, Joint rootJoint, Map jointMap) { + this.name = name; + this.jointCount = jointNumber; + this.rootJoint = rootJoint; + this.jointByName = jointMap; + this.jointById = new Int2ObjectOpenHashMap<>(); + this.pathIndexMap = Maps.newHashMap(); + + this.jointByName.values().forEach((joint) -> { + this.jointById.put(joint.getId(), joint); + }); + + this.poseMatrices = OpenMatrix4f.allocateMatrixArray(this.jointCount); + } + + protected Joint getOrLogException(Map jointMap, String name) { + if (!jointMap.containsKey(name)) { + if (EpicFightSharedConstants.IS_DEV_ENV) { + EpicFightMod.LOGGER.debug("Cannot find the joint named " + name + " in " + this.getClass().getCanonicalName()); + } + + return Joint.EMPTY; + } + + return jointMap.get(name); + } + + public void setPose(Pose pose) { + this.getPoseTransform(this.rootJoint, new OpenMatrix4f(), pose, this.poseMatrices, false); + } + + public void bakeOriginMatrices() { + this.rootJoint.initOriginTransform(new OpenMatrix4f()); + } + + public OpenMatrix4f[] getPoseMatrices() { + return this.poseMatrices; + } + + /** + * @param applyOriginTransform if you need a final pose of the animations, give it false. + */ + public OpenMatrix4f[] getPoseAsTransformMatrix(Pose pose, boolean applyOriginTransform) { + OpenMatrix4f[] jointMatrices = new OpenMatrix4f[this.jointCount]; + this.getPoseTransform(this.rootJoint, new OpenMatrix4f(), pose, jointMatrices, applyOriginTransform); + return jointMatrices; + } + + private void getPoseTransform(Joint joint, OpenMatrix4f parentTransform, Pose pose, OpenMatrix4f[] jointMatrices, boolean applyOriginTransform) { + OpenMatrix4f result = pose.orElseEmpty(joint.getName()).getAnimationBoundMatrix(joint, parentTransform); + jointMatrices[joint.getId()] = result; + + for (Joint joints : joint.getSubJoints()) { + this.getPoseTransform(joints, result, pose, jointMatrices, applyOriginTransform); + } + + if (applyOriginTransform) { + result.mulBack(joint.getToOrigin()); + } + } + + /** + * Inapposite past perfect + */ + @Deprecated(forRemoval = true, since = "1.21.1") + public OpenMatrix4f getBindedTransformFor(Pose pose, Joint joint) { + return this.getBoundTransformByJointIndex(pose, this.searchPathIndex(joint.getName()).createAccessTicket(this.rootJoint)); + } + + public OpenMatrix4f getBoundTransformFor(Pose pose, Joint joint) { + return this.getBoundTransformByJointIndex(pose, this.searchPathIndex(joint.getName()).createAccessTicket(this.rootJoint)); + } + + public OpenMatrix4f getBoundTransformByJointIndex(Pose pose, Joint.AccessTicket pathIndices) { + return this.getBoundJointTransformRecursively(pose, this.rootJoint, new OpenMatrix4f(), pathIndices); + } + + private OpenMatrix4f getBoundJointTransformRecursively(Pose pose, Joint joint, OpenMatrix4f parentTransform, Joint.AccessTicket pathIndices) { + JointTransform jt = pose.orElseEmpty(joint.getName()); + OpenMatrix4f result = jt.getAnimationBoundMatrix(joint, parentTransform); + + return pathIndices.hasNext() ? this.getBoundJointTransformRecursively(pose, pathIndices.next(), result, pathIndices) : result; + } + + public boolean hasJoint(String name) { + return this.jointByName.containsKey(name); + } + + public Joint searchJointById(int id) { + return this.jointById.get(id); + } + + public Joint searchJointByName(String name) { + return this.jointByName.get(name); + } + + /** + * Search and record joint path from root to terminal + * + * @param terminalJointName + * @return + */ + public Joint.HierarchicalJointAccessor searchPathIndex(String terminalJointName) { + return this.searchPathIndex(this.rootJoint, terminalJointName); + } + + /** + * Search and record joint path to terminal + * + * @param start + * @param terminalJointName + * @return + */ + public Joint.HierarchicalJointAccessor searchPathIndex(Joint start, String terminalJointName) { + String signature = start.getName() + "-" + terminalJointName; + + if (this.pathIndexMap.containsKey(signature)) { + return this.pathIndexMap.get(signature); + } else { + Joint.HierarchicalJointAccessor.Builder pathBuilder = start.searchPath(Joint.HierarchicalJointAccessor.builder(), terminalJointName); + Joint.HierarchicalJointAccessor accessor; + + if (pathBuilder == null) { + throw new IllegalArgumentException("Failed to get joint path index for " + terminalJointName); + } else { + accessor = pathBuilder.build(); + this.pathIndexMap.put(signature, accessor); + } + + return accessor; + } + } + + public void gatherAllJointsInPathToTerminal(String terminalJointName, Collection jointsInPath) { + if (!this.jointByName.containsKey(terminalJointName)) { + throw new NoSuchElementException("No " + terminalJointName + " joint in this armature!"); + } + + Joint.HierarchicalJointAccessor pathIndices = this.searchPathIndex(terminalJointName); + Joint.AccessTicket accessTicket = pathIndices.createAccessTicket(this.rootJoint); + + Joint joint = this.rootJoint; + jointsInPath.add(joint.getName()); + + while (accessTicket.hasNext()) { + jointsInPath.add(accessTicket.next().getName()); + } + } + + public int getJointNumber() { + return this.jointCount; + } + + @Override + public String toString() { + return this.name; + } + + public Armature deepCopy() { + Map oldToNewJoint = Maps.newHashMap(); + oldToNewJoint.put("empty", Joint.EMPTY); + + Joint newRoot = this.copyHierarchy(this.rootJoint, oldToNewJoint); + newRoot.initOriginTransform(new OpenMatrix4f()); + Armature newArmature = null; + + // Uses reflection to keep the type of copied armature + try { + Constructor constructor = this.getClass().getConstructor(String.class, int.class, Joint.class, Map.class); + newArmature = constructor.newInstance(this.name, this.jointCount, newRoot, oldToNewJoint); + } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IllegalStateException("Armature copy failed! " + e); + } + + return newArmature; + } + + private Joint copyHierarchy(Joint joint, Map oldToNewJoint) { + if (joint == Joint.EMPTY) { + return Joint.EMPTY; + } + + Joint newJoint = new Joint(joint.getName(), joint.getId(), joint.getLocalTransform()); + oldToNewJoint.put(joint.getName(), newJoint); + + for (Joint subJoint : joint.getSubJoints()) { + newJoint.addSubJoints(this.copyHierarchy(subJoint, oldToNewJoint)); + } + + return newJoint; + } + + public JsonObject toJsonObject() { + JsonObject root = new JsonObject(); + JsonObject armature = new JsonObject(); + + JsonArray jointNamesArray = new JsonArray(); + JsonArray jointHierarchy = new JsonArray(); + + this.jointById.int2ObjectEntrySet().stream().sorted((entry1, entry2) -> Integer.compare(entry1.getIntKey(), entry2.getIntKey())).forEach((entry) -> jointNamesArray.add(entry.getValue().getName())); + armature.add("joints", jointNamesArray); + armature.add("hierarchy", jointHierarchy); + + exportJoint(jointHierarchy, this.rootJoint, true); + + root.add("armature", armature); + + return root; + } + + private static void exportJoint(JsonArray parent, Joint joint, boolean root) { + JsonObject jointJson = new JsonObject(); + jointJson.addProperty("name", joint.getName()); + + JsonArray transformMatrix = new JsonArray(); + OpenMatrix4f localMatrixInBlender = new OpenMatrix4f(joint.getLocalTransform()); + + if (root) { + localMatrixInBlender.mulFront(OpenMatrix4f.invert(JsonAssetLoader.BLENDER_TO_MINECRAFT_COORD, null)); + } + + localMatrixInBlender.transpose(); + localMatrixInBlender.toList().forEach(transformMatrix::add); + jointJson.add("transform", transformMatrix); + parent.add(jointJson); + + if (!joint.getSubJoints().isEmpty()) { + JsonArray children = new JsonArray(); + jointJson.add("children", children); + joint.getSubJoints().forEach((joint$2) -> exportJoint(children, joint$2, false)); + } + } +} diff --git a/src/main/java/com/tiedup/remake/rig/armature/HumanoidArmature.java b/src/main/java/com/tiedup/remake/rig/armature/HumanoidArmature.java new file mode 100644 index 0000000..b3418be --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/armature/HumanoidArmature.java @@ -0,0 +1,119 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.armature; + +import java.util.Map; + +import com.tiedup.remake.rig.armature.Joint; +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.armature.types.HumanLikeArmature; + +public class HumanoidArmature extends Armature implements HumanLikeArmature { + public final Joint thighR; + public final Joint legR; + public final Joint kneeR; + public final Joint thighL; + public final Joint legL; + public final Joint kneeL; + public final Joint torso; + public final Joint chest; + public final Joint head; + public final Joint shoulderR; + public final Joint armR; + public final Joint handR; + public final Joint toolR; + public final Joint elbowR; + public final Joint shoulderL; + public final Joint armL; + public final Joint handL; + public final Joint toolL; + public final Joint elbowL; + + public HumanoidArmature(String name, int jointNumber, Joint rootJoint, Map jointMap) { + super(name, jointNumber, rootJoint, jointMap); + + this.thighR = this.getOrLogException(jointMap, "Thigh_R"); + this.legR = this.getOrLogException(jointMap, "Leg_R"); + this.kneeR = this.getOrLogException(jointMap, "Knee_R"); + this.thighL = this.getOrLogException(jointMap, "Thigh_L"); + this.legL = this.getOrLogException(jointMap, "Leg_L"); + this.kneeL = this.getOrLogException(jointMap, "Knee_L"); + this.torso = this.getOrLogException(jointMap, "Torso"); + this.chest = this.getOrLogException(jointMap, "Chest"); + this.head = this.getOrLogException(jointMap, "Head"); + this.shoulderR = this.getOrLogException(jointMap, "Shoulder_R"); + this.armR = this.getOrLogException(jointMap, "Arm_R"); + this.handR = this.getOrLogException(jointMap, "Hand_R"); + this.toolR = this.getOrLogException(jointMap, "Tool_R"); + this.elbowR = this.getOrLogException(jointMap, "Elbow_R"); + this.shoulderL = this.getOrLogException(jointMap, "Shoulder_L"); + this.armL = this.getOrLogException(jointMap, "Arm_L"); + this.handL = this.getOrLogException(jointMap, "Hand_L"); + this.toolL = this.getOrLogException(jointMap, "Tool_L"); + this.elbowL = this.getOrLogException(jointMap, "Elbow_L"); + } + + @Override + public Joint leftToolJoint() { + return this.toolL; + } + + @Override + public Joint rightToolJoint() { + return this.toolR; + } + + @Override + public Joint backToolJoint() { + return this.chest; + } + + @Override + public Joint leftHandJoint() { + return this.handL; + } + + @Override + public Joint rightHandJoint() { + return this.handR; + } + + @Override + public Joint leftArmJoint() { + return this.armL; + } + + @Override + public Joint rightArmJoint() { + return this.armR; + } + + @Override + public Joint leftLegJoint() { + return this.legL; + } + + @Override + public Joint rightLegJoint() { + return this.legR; + } + + @Override + public Joint leftThighJoint() { + return this.thighL; + } + + @Override + public Joint rightThighJoint() { + return this.thighR; + } + + @Override + public Joint headJoint() { + return this.head; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/armature/Joint.java b/src/main/java/com/tiedup/remake/rig/armature/Joint.java new file mode 100644 index 0000000..6bacd21 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/armature/Joint.java @@ -0,0 +1,278 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.function.Consumer; + +import javax.annotation.Nullable; + +import org.jetbrains.annotations.ApiStatus; + +import com.google.common.collect.Lists; + +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.math.OpenMatrix4f; + +public class Joint { + public static final Joint EMPTY = new Joint("empty", -1, new OpenMatrix4f()); + + private final List subJoints = Lists.newArrayList(); + private final int jointId; + private final String jointName; + private final OpenMatrix4f localTransform; + private final OpenMatrix4f toOrigin = new OpenMatrix4f(); + + public Joint(String name, int jointId, OpenMatrix4f localTransform) { + this.jointId = jointId; + this.jointName = name; + this.localTransform = localTransform.unmodifiable(); + } + + public void addSubJoints(Joint... joints) { + for (Joint joint : joints) { + if (!this.subJoints.contains(joint)) { + this.subJoints.add(joint); + } + } + } + + public void removeSubJoints(Joint... joints) { + for (Joint joint : joints) { + this.subJoints.remove(joint); + } + } + + public List getAllJoints() { + List list = Lists.newArrayList(); + this.getSubJoints(list); + + return list; + } + + public void iterSubJoints(Consumer iterTask) { + iterTask.accept(this); + + for (Joint joint : this.subJoints) { + joint.iterSubJoints(iterTask); + } + } + + private void getSubJoints(List list) { + list.add(this); + + for (Joint joint : this.subJoints) { + joint.getSubJoints(list); + } + } + + public void initOriginTransform(OpenMatrix4f parentTransform) { + OpenMatrix4f modelTransform = OpenMatrix4f.mul(parentTransform, this.localTransform, null); + OpenMatrix4f.invert(modelTransform, this.toOrigin); + + for (Joint joint : this.subJoints) { + joint.initOriginTransform(modelTransform); + } + } + + public OpenMatrix4f getLocalTransform() { + return this.localTransform; + } + + public OpenMatrix4f getToOrigin() { + return this.toOrigin; + } + + public List getSubJoints() { + return this.subJoints; + } + + // Null if index out of range + @Nullable + public Joint getSubJoint(int index) { + if (index < 0 || this.subJoints.size() <= index) { + return null; + } + + return this.subJoints.get(index); + } + + public String getName() { + return this.jointName; + } + + public int getId() { + return this.jointId; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Joint joint) { + return this.jointName.equals(joint.jointName) && this.jointId == joint.jointId; + } else { + return super.equals(o); + } + } + + @Override + public int hashCode() { + return this.jointName.hashCode() ^ this.jointId; + } + + /** + * Use the method that memorize path search results. {@link Armature#searchPathIndex(Joint, String)} + * + * @param builder + * @param jointName + * @return + */ + @ApiStatus.Internal + public HierarchicalJointAccessor.Builder searchPath(HierarchicalJointAccessor.Builder builder, String jointName) { + if (jointName.equals(this.getName())) { + return builder; + } else { + int i = 0; + + for (Joint subJoint : this.subJoints) { + HierarchicalJointAccessor.Builder nextBuilder = subJoint.searchPath(builder.append(i), jointName); + i++; + + if (nextBuilder != null) { + return nextBuilder; + } + } + + return null; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("\nid: " + this.jointId); + sb.append("\nname: " + this.jointName); + sb.append("\nlocal transform: " + this.localTransform); + sb.append("\nto origin: " + this.toOrigin); + sb.append("\nchildren: ["); + + int idx = 0; + + for (Joint joint : this.subJoints) { + idx++; + sb.append(joint.jointName); + + if (idx != this.subJoints.size()) { + sb.append(", "); + } + } + + sb.append("]\n"); + + return sb.toString(); + } + + public String printIncludingChildren() { + StringBuilder sb = new StringBuilder(); + sb.append(this.toString()); + + for (Joint joint : this.subJoints) { + sb.append(joint.printIncludingChildren()); + } + + return sb.toString(); + } + + public static class HierarchicalJointAccessor { + private Queue indicesToTerminal; + private final String signature; + + private HierarchicalJointAccessor(Builder builder) { + this.indicesToTerminal = builder.indicesToTerminal; + this.signature = builder.signature; + } + + public AccessTicket createAccessTicket(Joint rootJoint) { + return new AccessTicket(this.indicesToTerminal, rootJoint); + } + + @Override + public boolean equals(Object o) { + if (o instanceof HierarchicalJointAccessor accessor) { + this.signature.equals(accessor.signature); + } + + return super.equals(o); + } + + @Override + public int hashCode() { + return this.signature.hashCode(); + } + + public static Builder builder() { + return new Builder(new LinkedList<> (), ""); + } + + public static class Builder { + private Queue indicesToTerminal; + private String signature; + + private Builder(Queue indicesToTerminal, String signature) { + this.indicesToTerminal = indicesToTerminal; + this.signature = signature; + } + + public Builder append(int index) { + String signatureNext; + + if (this.indicesToTerminal.isEmpty()) { + signatureNext = this.signature + String.valueOf(index); + } else { + signatureNext = this.signature + "-" + String.valueOf(index); + } + + Queue nextQueue = new LinkedList<> (this.indicesToTerminal); + nextQueue.add(index); + + return new Builder(nextQueue, signatureNext); + } + + public HierarchicalJointAccessor build() { + return new HierarchicalJointAccessor(this); + } + } + } + + public static class AccessTicket implements Iterator { + Queue accecssStack; + Joint joint; + + private AccessTicket(Queue indicesToTerminal, Joint rootJoint) { + this.accecssStack = new LinkedList<> (indicesToTerminal); + this.joint = rootJoint; + } + + public boolean hasNext() { + return !this.accecssStack.isEmpty(); + } + + public Joint next() { + if (this.hasNext()) { + int nextIndex = this.accecssStack.poll(); + this.joint = this.joint.subJoints.get(nextIndex); + } else { + throw new NoSuchElementException(); + } + + return this.joint; + } + } +} diff --git a/src/main/java/com/tiedup/remake/rig/armature/JointTransform.java b/src/main/java/com/tiedup/remake/rig/armature/JointTransform.java new file mode 100644 index 0000000..a8a0f5b --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/armature/JointTransform.java @@ -0,0 +1,215 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import java.util.Map; + +import org.joml.Quaternionf; + +import com.google.common.collect.Maps; + +import net.minecraft.util.Mth; +import com.tiedup.remake.rig.math.AnimationTransformEntry; +import com.tiedup.remake.rig.math.MathUtils; +import com.tiedup.remake.rig.math.MatrixOperation; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; + +public class JointTransform { + public static final String ANIMATION_TRANSFORM = "animation_transform"; + public static final String JOINT_LOCAL_TRANSFORM = "joint_local_transform"; + public static final String PARENT = "parent"; + public static final String RESULT1 = "front_result"; + public static final String RESULT2 = "overwrite_rotation"; + + public static class TransformEntry { + public final MatrixOperation multiplyFunction; + public final JointTransform transform; + + public TransformEntry(MatrixOperation multiplyFunction, JointTransform transform) { + this.multiplyFunction = multiplyFunction; + this.transform = transform; + } + } + + private final Map entries = Maps.newHashMap(); + private final Vec3f translation; + private final Vec3f scale; + private final Quaternionf rotation; + + public JointTransform(Vec3f translation, Quaternionf rotation, Vec3f scale) { + this.translation = translation; + this.rotation = rotation; + this.scale = scale; + } + + public Vec3f translation() { + return this.translation; + } + + public Quaternionf rotation() { + return this.rotation; + } + + public Vec3f scale() { + return this.scale; + } + + public void clearTransform() { + this.translation.set(0.0F, 0.0F, 0.0F); + this.rotation.set(0.0F, 0.0F, 0.0F, 1.0F); + this.scale.set(1.0F, 1.0F, 1.0F); + } + + public JointTransform copy() { + return JointTransform.empty().copyFrom(this); + } + + public JointTransform copyFrom(JointTransform jt) { + Vec3f newV = jt.translation(); + Quaternionf newQ = jt.rotation(); + Vec3f newS = jt.scale; + this.translation.set(newV); + this.rotation.set(newQ); + this.scale.set(newS); + this.entries.putAll(jt.entries); + + return this; + } + + public void jointLocal(JointTransform transform, MatrixOperation multiplyFunction) { + this.entries.put(JOINT_LOCAL_TRANSFORM, new TransformEntry(multiplyFunction, this.mergeIfExist(JOINT_LOCAL_TRANSFORM, transform))); + } + + public void parent(JointTransform transform, MatrixOperation multiplyFunction) { + this.entries.put(PARENT, new TransformEntry(multiplyFunction, this.mergeIfExist(PARENT, transform))); + } + + public void animationTransform(JointTransform transform, MatrixOperation multiplyFunction) { + this.entries.put(ANIMATION_TRANSFORM, new TransformEntry(multiplyFunction, this.mergeIfExist(ANIMATION_TRANSFORM, transform))); + } + + public void frontResult(JointTransform transform, MatrixOperation multiplyFunction) { + this.entries.put(RESULT1, new TransformEntry(multiplyFunction, this.mergeIfExist(RESULT1, transform))); + } + + public void overwriteRotation(JointTransform transform) { + this.entries.put(RESULT2, new TransformEntry(OpenMatrix4f::mul, this.mergeIfExist(RESULT2, transform))); + } + + public JointTransform mergeIfExist(String entryName, JointTransform transform) { + if (this.entries.containsKey(entryName)) { + TransformEntry transformEntry = this.entries.get(entryName); + return JointTransform.mul(transform, transformEntry.transform, transformEntry.multiplyFunction); + } + + return transform; + } + + public OpenMatrix4f getAnimationBoundMatrix(Joint joint, OpenMatrix4f parentTransform) { + AnimationTransformEntry animationTransformEntry = new AnimationTransformEntry(); + + for (Map.Entry entry : this.entries.entrySet()) { + animationTransformEntry.put(entry.getKey(), entry.getValue().transform.toMatrix(), entry.getValue().multiplyFunction); + } + + animationTransformEntry.put(ANIMATION_TRANSFORM, this.toMatrix(), OpenMatrix4f::mul); + animationTransformEntry.put(JOINT_LOCAL_TRANSFORM, joint.getLocalTransform()); + animationTransformEntry.put(PARENT, parentTransform); + + return animationTransformEntry.getResult(); + } + + public OpenMatrix4f toMatrix() { + return new OpenMatrix4f().translate(this.translation).mulBack(OpenMatrix4f.fromQuaternion(this.rotation)).scale(this.scale); + } + + @Override + public String toString() { + return String.format("translation:%s, rotation:%s, scale:%s %d entries ", this.translation, this.rotation, this.scale, this.entries.size()); + } + + public static JointTransform interpolateTransform(JointTransform prev, JointTransform next, float progression, JointTransform dest) { + if (dest == null) { + dest = JointTransform.empty(); + } + + MathUtils.lerpVector(prev.translation, next.translation, progression, dest.translation); + MathUtils.lerpQuaternion(prev.rotation, next.rotation, progression, dest.rotation); + MathUtils.lerpVector(prev.scale, next.scale, progression, dest.scale); + + return dest; + } + + public static JointTransform interpolate(JointTransform prev, JointTransform next, float progression) { + return interpolate(prev, next, progression, null); + } + + public static JointTransform interpolate(JointTransform prev, JointTransform next, float progression, JointTransform dest) { + if (dest == null) { + dest = JointTransform.empty(); + } + + if (prev == null || next == null) { + dest.clearTransform(); + return dest; + } + + progression = Mth.clamp(progression, 0.0F, 1.0F); + interpolateTransform(prev, next, progression, dest); + dest.entries.clear(); + + for (Map.Entry entry : prev.entries.entrySet()) { + JointTransform transform = next.entries.containsKey(entry.getKey()) ? next.entries.get(entry.getKey()).transform : JointTransform.empty(); + dest.entries.put(entry.getKey(), new TransformEntry(entry.getValue().multiplyFunction, interpolateTransform(entry.getValue().transform, transform, progression, null))); + } + + for (Map.Entry entry : next.entries.entrySet()) { + if (!dest.entries.containsKey(entry.getKey())) { + dest.entries.put(entry.getKey(), new TransformEntry(entry.getValue().multiplyFunction, interpolateTransform(JointTransform.empty(), entry.getValue().transform, progression, null))); + } + } + + return dest; + } + + public static JointTransform fromMatrixWithoutScale(OpenMatrix4f matrix) { + return new JointTransform(matrix.toTranslationVector(), matrix.toQuaternion(), new Vec3f(1.0F, 1.0F, 1.0F)); + } + + public static JointTransform translation(Vec3f vec) { + return JointTransform.translationRotation(vec, new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F)); + } + + public static JointTransform rotation(Quaternionf quat) { + return JointTransform.translationRotation(new Vec3f(0.0F, 0.0F, 0.0F), quat); + } + + public static JointTransform scale(Vec3f vec) { + return new JointTransform(new Vec3f(0.0F, 0.0F, 0.0F), new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F), vec); + } + + public static JointTransform fromMatrix(OpenMatrix4f matrix) { + return new JointTransform(matrix.toTranslationVector(), matrix.toQuaternion(), matrix.toScaleVector()); + } + + public static JointTransform translationRotation(Vec3f vec, Quaternionf quat) { + return new JointTransform(vec, quat, new Vec3f(1.0F, 1.0F, 1.0F)); + } + + public static JointTransform mul(JointTransform left, JointTransform right, MatrixOperation operation) { + return JointTransform.fromMatrix(operation.mul(left.toMatrix(), right.toMatrix(), null)); + } + + public static JointTransform fromPrimitives(float locX, float locY, float locZ, float quatX, float quatY, float quatZ, float quatW, float scaX, float scaY, float scaZ) { + return new JointTransform(new Vec3f(locX, locY, locZ), new Quaternionf(quatX, quatY, quatZ, quatW), new Vec3f(scaX, scaY, scaZ)); + } + + public static JointTransform empty() { + return new JointTransform(new Vec3f(0.0F, 0.0F, 0.0F), new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F), new Vec3f(1.0F, 1.0F, 1.0F)); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/armature/types/HumanLikeArmature.java b/src/main/java/com/tiedup/remake/rig/armature/types/HumanLikeArmature.java new file mode 100644 index 0000000..ec3b98b --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/armature/types/HumanLikeArmature.java @@ -0,0 +1,25 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.armature.types; + +import com.tiedup.remake.rig.armature.Joint; + +/** + * This class is not being used by Epic Fight, but is left to meet various purposes of developers + * Also presents developers which joints are necessary when an armature would be Human-like + */ +public interface HumanLikeArmature extends ToolHolderArmature { + public Joint leftHandJoint(); + public Joint rightHandJoint(); + public Joint leftArmJoint(); + public Joint rightArmJoint(); + public Joint leftLegJoint(); + public Joint rightLegJoint(); + public Joint leftThighJoint(); + public Joint rightThighJoint(); + public Joint headJoint(); +} diff --git a/src/main/java/com/tiedup/remake/rig/asset/AssetAccessor.java b/src/main/java/com/tiedup/remake/rig/asset/AssetAccessor.java new file mode 100644 index 0000000..57e3d49 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/asset/AssetAccessor.java @@ -0,0 +1,69 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.asset; + +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import net.minecraft.resources.ResourceLocation; + +/** + * An accessor class + * @param {@link Object} can be any object + */ +public interface AssetAccessor extends Supplier { + O get(); + + ResourceLocation registryName(); + + default boolean isPresent() { + return this.get() != null; + } + + default boolean isEmpty() { + return !this.isPresent(); + } + + boolean inRegistry(); + + default boolean checkType(Class cls) { + return cls.isAssignableFrom(this.get().getClass()); + } + + default O orElse(O whenNull) { + return this.isPresent() ? this.get() : whenNull; + } + + default void ifPresent(Consumer action) { + if (this.isPresent()) { + action.accept(this.get()); + } + } + + default void ifPresentOrElse(Consumer action, Runnable whenNull) { + if (this.isPresent()) { + action.accept(this.get()); + } else { + whenNull.run(); + } + } + + default void doOrThrow(Consumer action) { + if (this.isPresent()) { + action.accept(this.get()); + } else { + throw new NoSuchElementException("No asset " + this.registryName()); + } + } + + default void checkNotNull() { + if (!this.isPresent()) { + throw new NoSuchElementException("No asset " + this.registryName()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/asset/JsonAssetLoader.java b/src/main/java/com/tiedup/remake/rig/asset/JsonAssetLoader.java new file mode 100644 index 0000000..24d83bd --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/asset/JsonAssetLoader.java @@ -0,0 +1,833 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.asset; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonReader; + +import io.netty.util.internal.StringUtil; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.GsonHelper; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.tiedup.remake.rig.anim.AnimationClip; +import com.tiedup.remake.rig.armature.Joint; +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.anim.Keyframe; +import com.tiedup.remake.rig.anim.TransformSheet; +import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty; +import com.tiedup.remake.rig.anim.types.ActionAnimation; +import com.tiedup.remake.rig.anim.types.AttackAnimation; +import com.tiedup.remake.rig.anim.types.AttackAnimation.Phase; +import com.tiedup.remake.rig.anim.types.MainFrameAnimation; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.mesh.ClassicMesh; +import com.tiedup.remake.rig.mesh.CompositeMesh; +import com.tiedup.remake.rig.mesh.Mesh; +import com.tiedup.remake.rig.mesh.MeshPartDefinition; +import com.tiedup.remake.rig.mesh.Meshes; +import com.tiedup.remake.rig.mesh.Meshes.MeshContructor; +import com.tiedup.remake.rig.mesh.SkinnedMesh; +import com.tiedup.remake.rig.mesh.SoftBodyTranslatable; +import com.tiedup.remake.rig.mesh.StaticMesh; +import com.tiedup.remake.rig.mesh.VertexBuilder; +import com.tiedup.remake.rig.mesh.transformer.VanillaModelTransformer.VanillaMeshPartDefinition; +import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject.ClothPart.ConstraintType; +import com.tiedup.remake.rig.exception.AssetLoadingException; +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.util.ParseUtil; +import com.tiedup.remake.rig.math.MathUtils; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; +import com.tiedup.remake.rig.math.Vec4f; +import yesman.epicfight.gameasset.Armatures.ArmatureContructor; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.main.EpicFightSharedConstants; + +public class JsonAssetLoader { + public static final OpenMatrix4f BLENDER_TO_MINECRAFT_COORD = OpenMatrix4f.createRotatorDeg(-90.0F, Vec3f.X_AXIS); + public static final OpenMatrix4f MINECRAFT_TO_BLENDER_COORD = OpenMatrix4f.invert(BLENDER_TO_MINECRAFT_COORD, null); + public static final String UNGROUPED_NAME = "noGroups"; + public static final String COORD_BONE = "Coord"; + public static final String ROOT_BONE = "Root"; + + private JsonObject rootJson; + + // Used for deciding armature name, other resources are nullable + @Nullable + private ResourceLocation resourceLocation; + private String filehash; + + public JsonAssetLoader(ResourceManager resourceManager, ResourceLocation resourceLocation) throws AssetLoadingException { + JsonReader jsonReader = null; + this.resourceLocation = resourceLocation; + + try { + try { + if (resourceManager == null) { + throw new NoSuchElementException(); + } + + Resource resource = resourceManager.getResource(resourceLocation).orElseThrow(); + InputStream inputStream = resource.open(); + InputStreamReader isr = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + + jsonReader = new JsonReader(isr); + jsonReader.setLenient(true); + this.rootJson = Streams.parse(jsonReader).getAsJsonObject(); + } catch (NoSuchElementException e) { + // In this case, reads the animation data from mod.jar (Especially in a server) + Class modClass = ModList.get().getModObjectById(resourceLocation.getNamespace()).orElseThrow(() -> new AssetLoadingException("No modid " + resourceLocation)).getClass(); + InputStream inputStream = modClass.getResourceAsStream("/assets/" + resourceLocation.getNamespace() + "/" + resourceLocation.getPath()); + + if (inputStream == null) { + modClass = ModList.get().getModObjectById(EpicFightMod.MODID).get().getClass(); + inputStream = modClass.getResourceAsStream("/assets/" + resourceLocation.getNamespace() + "/" + resourceLocation.getPath()); + } + + //Still null, throws exception. + if (inputStream == null) { + throw new AssetLoadingException("Can't find resource file: " + resourceLocation); + } + + BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); + InputStreamReader reader = new InputStreamReader(bufferedInputStream, StandardCharsets.UTF_8); + + jsonReader = new JsonReader(reader); + jsonReader.setLenient(true); + this.rootJson = Streams.parse(jsonReader).getAsJsonObject(); + } + } catch (IOException e) { + throw new AssetLoadingException("Can't read " + resourceLocation.toString() + " because of " + e); + } finally { + if (jsonReader != null) { + try { + jsonReader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + this.filehash = ParseUtil.getBytesSHA256Hash(this.rootJson.toString().getBytes()); + } + + @OnlyIn(Dist.CLIENT) + public JsonAssetLoader(InputStream inputstream, ResourceLocation resourceLocation) throws AssetLoadingException { + JsonReader jsonReader = null; + this.resourceLocation = resourceLocation; + + jsonReader = new JsonReader(new InputStreamReader(inputstream, StandardCharsets.UTF_8)); + jsonReader.setLenient(true); + this.rootJson = Streams.parse(jsonReader).getAsJsonObject(); + + try { + jsonReader.close(); + } catch (IOException e) { + throw new AssetLoadingException("Can't read " + resourceLocation.toString() + ": " + e); + } + + this.filehash = StringUtil.EMPTY_STRING; + } + + @OnlyIn(Dist.CLIENT) + public JsonAssetLoader(JsonObject rootJson, ResourceLocation rl) { + this.rootJson = rootJson; + this.resourceLocation = rl; + this.filehash = StringUtil.EMPTY_STRING; + } + + @OnlyIn(Dist.CLIENT) + public static Mesh.RenderProperties getRenderProperties(JsonObject json) { + if (!json.has("render_properties")) { + return null; + } + + JsonObject properties = json.getAsJsonObject("render_properties"); + Mesh.RenderProperties.Builder renderProperties = Mesh.RenderProperties.Builder.create(); + + if (properties.has("transparent")) { + renderProperties.transparency(properties.get("transparent").getAsBoolean()); + } + + if (properties.has("texture_path")) { + renderProperties.customTexturePath(properties.get("texture_path").getAsString()); + } + + if (properties.has("color")) { + JsonArray jsonarray = properties.getAsJsonArray("color"); + renderProperties.customColor(jsonarray.get(0).getAsFloat(), jsonarray.get(1).getAsFloat(), jsonarray.get(2).getAsFloat()); + } + + return renderProperties.build(); + } + + @OnlyIn(Dist.CLIENT) + public ResourceLocation getParent() { + return this.rootJson.has("parent") ? ResourceLocation.parse(this.rootJson.get("parent").getAsString()) : null; + } + + private static final float DEFAULT_PARTICLE_MASS = 0.16F; + private static final float DEFAULT_SELF_COLLISON = 0.05F; + + @Nullable + @OnlyIn(Dist.CLIENT) + public Map loadClothInformation(Float[] positionArray) { + JsonObject obj = this.rootJson.getAsJsonObject("vertices"); + JsonObject clothInfoObj = obj.getAsJsonObject("cloth_info"); + + if (clothInfoObj == null) { + return null; + } + + Map clothInfo = Maps.newHashMap(); + + for (Map.Entry e : clothInfoObj.entrySet()) { + JsonObject clothObject = e.getValue().getAsJsonObject(); + int[] particlesArray = ParseUtil.toIntArrayPrimitive(clothObject.get("particles").getAsJsonObject().get("array").getAsJsonArray()); + float[] weightsArray = ParseUtil.toFloatArrayPrimitive(clothObject.get("weights").getAsJsonObject().get("array").getAsJsonArray()); + float particleMass = clothObject.has("particle_mass") ? clothObject.get("particle_mass").getAsFloat() : DEFAULT_PARTICLE_MASS; + float selfCollision = clothObject.has("self_collision") ? clothObject.get("self_collision").getAsFloat() : DEFAULT_SELF_COLLISON; + + JsonArray constraintsArray = clothObject.get("constraints").getAsJsonArray(); + List constraintsList = new ArrayList<> (constraintsArray.size()); + float[] compliances = new float[constraintsArray.size()]; + ConstraintType[] constraintType = new ConstraintType[constraintsArray.size()]; + float[] rootDistances = new float[particlesArray.length / 2]; + + int i = 0; + + for (JsonElement element : constraintsArray) { + JsonObject asJsonObject = element.getAsJsonObject(); + + if (asJsonObject.has("unused") && GsonHelper.getAsBoolean(asJsonObject, "unused")) { + continue; + } + + constraintType[i] = ConstraintType.valueOf(GsonHelper.getAsString(asJsonObject, "type").toUpperCase(Locale.ROOT)); + compliances[i] = GsonHelper.getAsFloat(asJsonObject, "compliance"); + constraintsList.add(ParseUtil.toIntArrayPrimitive(asJsonObject.get("array").getAsJsonArray())); + element.getAsJsonObject().get("compliance"); + i++; + } + + List rootParticles = Lists.newArrayList(); + + for (int j = 0; j < particlesArray.length / 2; j++) { + int weightIndex = particlesArray[j * 2 + 1]; + float weight = weightsArray[weightIndex]; + + if (weight == 0.0F) { + int posId = particlesArray[j * 2]; + rootParticles.add(new Vec3(positionArray[posId * 3 + 0], positionArray[posId * 3 + 1], positionArray[posId * 3 + 2])); + } + } + + for (int j = 0; j < particlesArray.length / 2; j++) { + int posId = particlesArray[j * 2]; + Vec3 position = new Vec3(positionArray[posId * 3 + 0], positionArray[posId * 3 + 1], positionArray[posId * 3 + 2]); + Vec3 nearest = MathUtils.getNearestVector(position, rootParticles); + rootDistances[j] = (float)position.distanceTo(nearest); + } + + int[] normalOffsetMappingArray = null; + + if (clothObject.has("normal_offsets")) { + normalOffsetMappingArray = ParseUtil.toIntArrayPrimitive(clothObject.get("normal_offsets").getAsJsonObject().get("array").getAsJsonArray()); + } + + SoftBodyTranslatable.ClothSimulationInfo clothSimulInfo = new SoftBodyTranslatable.ClothSimulationInfo(particleMass, selfCollision, constraintsList, constraintType, compliances, particlesArray, weightsArray, rootDistances, normalOffsetMappingArray); + clothInfo.put(e.getKey(), clothSimulInfo); + } + + return clothInfo; + } + + @OnlyIn(Dist.CLIENT) + public T loadClassicMesh(MeshContructor constructor) { + ResourceLocation parent = this.getParent(); + + if (parent != null) { + T mesh = Meshes.getOrCreate(parent, (jsonLoader) -> jsonLoader.loadClassicMesh(constructor)).get(); + return constructor.invoke(null, null, mesh, getRenderProperties(this.rootJson)); + } else { + JsonObject obj = this.rootJson.getAsJsonObject("vertices"); + JsonObject positions = obj.getAsJsonObject("positions"); + JsonObject normals = obj.getAsJsonObject("normals"); + JsonObject uvs = obj.getAsJsonObject("uvs"); + JsonObject parts = obj.getAsJsonObject("parts"); + JsonObject indices = obj.getAsJsonObject("indices"); + Float[] positionArray = ParseUtil.toFloatArray(positions.get("array").getAsJsonArray()); + + for (int i = 0; i < positionArray.length / 3; i++) { + int k = i * 3; + Vec4f posVector = new Vec4f(positionArray[k], positionArray[k+1], positionArray[k+2], 1.0F); + OpenMatrix4f.transform(BLENDER_TO_MINECRAFT_COORD, posVector, posVector); + positionArray[k] = posVector.x; + positionArray[k+1] = posVector.y; + positionArray[k+2] = posVector.z; + } + + Float[] normalArray = ParseUtil.toFloatArray(normals.get("array").getAsJsonArray()); + + for (int i = 0; i < normalArray.length / 3; i++) { + int k = i * 3; + Vec4f normVector = new Vec4f(normalArray[k], normalArray[k+1], normalArray[k+2], 1.0F); + OpenMatrix4f.transform(BLENDER_TO_MINECRAFT_COORD, normVector, normVector); + normalArray[k] = normVector.x; + normalArray[k+1] = normVector.y; + normalArray[k+2] = normVector.z; + } + + Float[] uvArray = ParseUtil.toFloatArray(uvs.get("array").getAsJsonArray()); + + Map arrayMap = Maps.newHashMap(); + Map> meshMap = Maps.newHashMap(); + + arrayMap.put("positions", positionArray); + arrayMap.put("normals", normalArray); + arrayMap.put("uvs", uvArray); + + if (parts != null) { + for (Map.Entry e : parts.entrySet()) { + meshMap.put(VanillaMeshPartDefinition.of(e.getKey(), getRenderProperties(e.getValue().getAsJsonObject())), VertexBuilder.create(ParseUtil.toIntArrayPrimitive(e.getValue().getAsJsonObject().get("array").getAsJsonArray()))); + } + } + + if (indices != null) { + meshMap.put(VanillaMeshPartDefinition.of(UNGROUPED_NAME), VertexBuilder.create(ParseUtil.toIntArrayPrimitive(indices.get("array").getAsJsonArray()))); + } + + T mesh = constructor.invoke(arrayMap, meshMap, null, getRenderProperties(this.rootJson)); + mesh.putSoftBodySimulationInfo(this.loadClothInformation(positionArray)); + + return mesh; + } + } + + @OnlyIn(Dist.CLIENT) + public T loadSkinnedMesh(MeshContructor constructor) { + ResourceLocation parent = this.getParent(); + + if (parent != null) { + T mesh = Meshes.getOrCreate(parent, (jsonLoader) -> jsonLoader.loadSkinnedMesh(constructor)).get(); + return constructor.invoke(null, null, mesh, getRenderProperties(this.rootJson)); + } else { + JsonObject obj = this.rootJson.getAsJsonObject("vertices"); + JsonObject positions = obj.getAsJsonObject("positions"); + JsonObject normals = obj.getAsJsonObject("normals"); + JsonObject uvs = obj.getAsJsonObject("uvs"); + JsonObject vdincies = obj.getAsJsonObject("vindices"); + JsonObject weights = obj.getAsJsonObject("weights"); + JsonObject vcounts = obj.getAsJsonObject("vcounts"); + JsonObject parts = obj.getAsJsonObject("parts"); + JsonObject indices = obj.getAsJsonObject("indices"); + + Float[] positionArray = ParseUtil.toFloatArray(positions.get("array").getAsJsonArray()); + + for (int i = 0; i < positionArray.length / 3; i++) { + int k = i * 3; + Vec4f posVector = new Vec4f(positionArray[k], positionArray[k+1], positionArray[k+2], 1.0F); + OpenMatrix4f.transform(BLENDER_TO_MINECRAFT_COORD, posVector, posVector); + positionArray[k] = posVector.x; + positionArray[k+1] = posVector.y; + positionArray[k+2] = posVector.z; + } + + Float[] normalArray = ParseUtil.toFloatArray(normals.get("array").getAsJsonArray()); + + for (int i = 0; i < normalArray.length / 3; i++) { + int k = i * 3; + Vec4f normVector = new Vec4f(normalArray[k], normalArray[k+1], normalArray[k+2], 1.0F); + OpenMatrix4f.transform(BLENDER_TO_MINECRAFT_COORD, normVector, normVector); + normalArray[k] = normVector.x; + normalArray[k+1] = normVector.y; + normalArray[k+2] = normVector.z; + } + + Float[] uvArray = ParseUtil.toFloatArray(uvs.get("array").getAsJsonArray()); + Float[] weightArray = ParseUtil.toFloatArray(weights.get("array").getAsJsonArray()); + Integer[] affectingJointCounts = ParseUtil.toIntArray(vcounts.get("array").getAsJsonArray()); + Integer[] affectingJointIndices = ParseUtil.toIntArray(vdincies.get("array").getAsJsonArray()); + + Map arrayMap = Maps.newHashMap(); + Map> meshMap = Maps.newHashMap(); + arrayMap.put("positions", positionArray); + arrayMap.put("normals", normalArray); + arrayMap.put("uvs", uvArray); + arrayMap.put("weights", weightArray); + arrayMap.put("vcounts", affectingJointCounts); + arrayMap.put("vindices", affectingJointIndices); + + if (parts != null) { + for (Map.Entry e : parts.entrySet()) { + meshMap.put(VanillaMeshPartDefinition.of(e.getKey(), getRenderProperties(e.getValue().getAsJsonObject())), VertexBuilder.create(ParseUtil.toIntArrayPrimitive(e.getValue().getAsJsonObject().get("array").getAsJsonArray()))); + } + } + + if (indices != null) { + meshMap.put(VanillaMeshPartDefinition.of(UNGROUPED_NAME), VertexBuilder.create(ParseUtil.toIntArrayPrimitive(indices.get("array").getAsJsonArray()))); + } + + T mesh = constructor.invoke(arrayMap, meshMap, null, getRenderProperties(this.rootJson)); + mesh.putSoftBodySimulationInfo(this.loadClothInformation(positionArray)); + + return mesh; + } + } + + @OnlyIn(Dist.CLIENT) + public CompositeMesh loadCompositeMesh() throws AssetLoadingException { + if (!this.rootJson.has("meshes")) { + throw new AssetLoadingException("Composite mesh loading exception: lower meshes undefined"); + } + + JsonAssetLoader clothLoader = new JsonAssetLoader(this.rootJson.get("meshes").getAsJsonObject().get("cloth").getAsJsonObject(), null); + JsonAssetLoader staticLoader = new JsonAssetLoader(this.rootJson.get("meshes").getAsJsonObject().get("static").getAsJsonObject(), null); + SoftBodyTranslatable softBodyMesh = (SoftBodyTranslatable)clothLoader.loadMesh(false); + StaticMesh staticMesh = (StaticMesh)staticLoader.loadMesh(false); + + if (!softBodyMesh.canStartSoftBodySimulation()) { + throw new AssetLoadingException("Composite mesh loading exception: soft mesh doesn't have cloth info"); + } + + return new CompositeMesh(staticMesh, softBodyMesh); + } + + @OnlyIn(Dist.CLIENT) + public Mesh loadMesh() throws AssetLoadingException { + return this.loadMesh(true); + } + + @OnlyIn(Dist.CLIENT) + private Mesh loadMesh(boolean allowCompositeMesh) throws AssetLoadingException { + if (!this.rootJson.has("mesh_loader")) { + throw new AssetLoadingException("Mesh loading exception: No mesh loader provided!"); + } + + String loader = this.rootJson.get("mesh_loader").getAsString(); + + switch (loader) { + case "classic_mesh" -> { + return this.loadClassicMesh(ClassicMesh::new); + } + case "skinned_mesh" -> { + return this.loadSkinnedMesh(SkinnedMesh::new); + } + case "composite_mesh" -> { + if (!allowCompositeMesh) { + throw new AssetLoadingException("Can't have a composite mesh inside another composite mesh"); + } + + return this.loadCompositeMesh(); + } + default -> { + throw new AssetLoadingException("Mesh loading exception: Unsupported mesh loader: " + loader); + } + } + } + + public T loadArmature(ArmatureContructor constructor) { + if (this.resourceLocation == null) { + throw new AssetLoadingException("Can't load armature: Resource location is null."); + } + + JsonObject obj = this.rootJson.getAsJsonObject("armature"); + TransformFormat transformFormat = getAsTransformFormatOrDefault(obj, "armature_format"); + JsonObject hierarchy = obj.get("hierarchy").getAsJsonArray().get(0).getAsJsonObject(); + JsonArray nameAsVertexGroups = obj.getAsJsonArray("joints"); + Map jointIds = Maps.newHashMap(); + + int id = 0; + + for (int i = 0; i < nameAsVertexGroups.size(); i++) { + String name = nameAsVertexGroups.get(i).getAsString(); + + if (name.equals(COORD_BONE)) { + continue; + } + + jointIds.put(name, id); + id++; + } + + Map jointMap = Maps.newHashMap(); + Joint joint = getJoint(hierarchy, jointIds, jointMap, transformFormat, true); + joint.initOriginTransform(new OpenMatrix4f()); + + String armatureName = this.resourceLocation.toString().replaceAll("(animmodels/|\\.json)", ""); + + return constructor.invoke(armatureName, jointMap.size(), joint, jointMap); + } + + private static Joint getJoint(JsonObject object, Map jointIdMap, Map jointMap, TransformFormat transformFormat, boolean root) { + String name = object.get("name").getAsString(); + + if (!jointIdMap.containsKey(name)) { + throw new AssetLoadingException("Can't load joint: joint name " + name + " doesn't exist in armature hierarchy."); + } + + // Skip Coord bone + if (name.equals(COORD_BONE)) { + JsonArray coordChildren = object.get("children").getAsJsonArray(); + + if (coordChildren.isEmpty()) { + throw new AssetLoadingException("No children for Coord bone"); + } else if (coordChildren.size() > 1) { + throw new AssetLoadingException("Coord bone can't have multiple children"); + } else { + return getJoint(coordChildren.get(0).getAsJsonObject(), jointIdMap, jointMap, transformFormat, false); + } + } + + JsonElement transform = GsonHelper.getNonNull(object, "transform"); + + // WORKAROUND: The case when transform format is wrongly specified! + if (transformFormat == TransformFormat.ATTRIBUTES && transform.isJsonArray()) { + transformFormat = TransformFormat.MATRIX; + } else if (transformFormat == TransformFormat.MATRIX && transform.isJsonObject()) { + transformFormat = TransformFormat.ATTRIBUTES; + } + + OpenMatrix4f localMatrix = null; + + switch (transformFormat) { + case MATRIX -> { + float[] matrixElements = ParseUtil.toFloatArrayPrimitive(GsonHelper.convertToJsonArray(transform, "transform")); + localMatrix = OpenMatrix4f.load(null, matrixElements); + localMatrix.transpose(); + + if (root) { + localMatrix.mulFront(BLENDER_TO_MINECRAFT_COORD); + } + } + case ATTRIBUTES -> { + JsonObject transformObject = transform.getAsJsonObject(); + JsonArray locArray = transformObject.get("loc").getAsJsonArray(); + JsonArray rotArray = transformObject.get("rot").getAsJsonArray(); + JsonArray scaArray = transformObject.get("sca").getAsJsonArray(); + JointTransform jointTransform + = JointTransform.fromPrimitives( + locArray.get(0).getAsFloat() + , locArray.get(1).getAsFloat() + , locArray.get(2).getAsFloat() + , -rotArray.get(1).getAsFloat() + , -rotArray.get(2).getAsFloat() + , -rotArray.get(3).getAsFloat() + , rotArray.get(0).getAsFloat() + , scaArray.get(0).getAsFloat() + , scaArray.get(1).getAsFloat() + , scaArray.get(2).getAsFloat() + ); + + localMatrix = jointTransform.toMatrix(); + + if (root) { + localMatrix.mulFront(BLENDER_TO_MINECRAFT_COORD); + } + } + } + + Joint joint = new Joint(name, jointIdMap.get(name), localMatrix); + jointMap.put(name, joint); + + if (object.has("children")) { + for (JsonElement children : object.get("children").getAsJsonArray()) { + joint.addSubJoints(getJoint(children.getAsJsonObject(), jointIdMap, jointMap, transformFormat, false)); + } + } + + return joint; + } + + public AnimationClip loadClipForAnimation(StaticAnimation animation) { + if (this.rootJson == null) { + throw new AssetLoadingException("Can't find animation in path: " + animation); + } + + if (animation.getArmature() == null) { + EpicFightMod.LOGGER.error("Animation " + animation + " doesn't have an armature."); + } + + TransformFormat format = getAsTransformFormatOrDefault(this.rootJson, "format"); + JsonArray array = this.rootJson.get("animation").getAsJsonArray(); + boolean action = animation instanceof MainFrameAnimation; + boolean attack = animation instanceof AttackAnimation; + boolean noTransformData = !action && !attack && FMLEnvironment.dist == Dist.DEDICATED_SERVER; + boolean root = true; + Armature armature = animation.getArmature().get(); + Set allowedJoints = Sets.newLinkedHashSet(); + + if (attack) { + for (Phase phase : ((AttackAnimation)animation).phases) { + for (AttackAnimation.JointColliderPair colliderInfo : phase.getColliders()) { + armature.gatherAllJointsInPathToTerminal(colliderInfo.getFirst().getName(), allowedJoints); + } + } + } else if (action) { + allowedJoints.add(ROOT_BONE); + } + + AnimationClip clip = new AnimationClip(); + + for (JsonElement element : array) { + JsonObject jObject = element.getAsJsonObject(); + String name = jObject.get("name").getAsString(); + + if (attack && FMLEnvironment.dist == Dist.DEDICATED_SERVER && !allowedJoints.contains(name)) { + if (name.equals(COORD_BONE)) { + root = false; + } + + continue; + } + + Joint joint = armature.searchJointByName(name); + + if (joint == null) { + if (name.equals(COORD_BONE)) { + TransformSheet sheet = getTransformSheet(jObject, new OpenMatrix4f(), true, format); + + if (action) { + ((ActionAnimation)animation).addProperty(ActionAnimationProperty.COORD, sheet); + } + + root = false; + continue; + } else { + EpicFightMod.LOGGER.debug("[EpicFightMod] No joint named " + name + " in " + animation); + continue; + } + } + + TransformSheet sheet = getTransformSheet(jObject, OpenMatrix4f.invert(joint.getLocalTransform(), null), root, format); + + if (!noTransformData) { + clip.addJointTransform(name, sheet); + } + + float maxFrameTime = sheet.maxFrameTime(); + + if (clip.getClipTime() < maxFrameTime) { + clip.setClipTime(maxFrameTime); + } + + root = false; + } + + return clip; + } + + public AnimationClip loadAllJointsClipForAnimation(StaticAnimation animation) { + TransformFormat format = getAsTransformFormatOrDefault(this.rootJson, "format"); + JsonArray array = this.rootJson.get("animation").getAsJsonArray(); + boolean root = true; + + if (animation.getArmature() == null) { + EpicFightMod.LOGGER.error("Animation " + animation + " doesn't have an armature."); + } + + Armature armature = animation.getArmature().get(); + AnimationClip clip = new AnimationClip(); + + for (JsonElement element : array) { + JsonObject jObject = element.getAsJsonObject(); + String name = jObject.get("name").getAsString(); + Joint joint = armature.searchJointByName(name); + + if (joint == null) { + if (EpicFightSharedConstants.IS_DEV_ENV) { + EpicFightMod.LOGGER.debug(animation.getRegistryName() + ": No joint named " + name + " in armature"); + } + + continue; + } + + TransformSheet sheet = getTransformSheet(jObject, OpenMatrix4f.invert(joint.getLocalTransform(), null), root, format); + clip.addJointTransform(name, sheet); + float maxFrameTime = sheet.maxFrameTime(); + + if (clip.getClipTime() < maxFrameTime) { + clip.setClipTime(maxFrameTime); + } + + root = false; + } + + return clip; + } + + public JsonObject getRootJson() { + return this.rootJson; + } + + public String getFileHash() { + return this.filehash; + } + + public static TransformFormat getAsTransformFormatOrDefault(JsonObject jsonObject, String propertyName) { + return jsonObject.has(propertyName) ? ParseUtil.enumValueOfOrNull(TransformFormat.class, GsonHelper.getAsString(jsonObject, propertyName)) : TransformFormat.MATRIX; + } + + public AnimationClip loadAnimationClip(Armature armature) { + TransformFormat format = getAsTransformFormatOrDefault(this.rootJson, "format"); + JsonArray array = this.rootJson.get("animation").getAsJsonArray(); + AnimationClip clip = new AnimationClip(); + boolean root = true; + + for (JsonElement element : array) { + JsonObject jObject = element.getAsJsonObject(); + String name = jObject.get("name").getAsString(); + Joint joint = armature.searchJointByName(name); + + if (joint == null) { + continue; + } + + TransformSheet sheet = getTransformSheet(element.getAsJsonObject(), OpenMatrix4f.invert(joint.getLocalTransform(), null), root, format); + clip.addJointTransform(name, sheet); + float maxFrameTime = sheet.maxFrameTime(); + + if (clip.getClipTime() < maxFrameTime) { + clip.setClipTime(maxFrameTime); + } + + root = false; + } + + return clip; + } + + /** + * @param jObject + * @param invLocalTransform nullable if transformFormat == {@link TransformFormat#ATTRIBUTES} + * @param rootCorrection no matter what the value is if transformFormat == {@link TransformFormat#ATTRIBUTES} + * @param transformFormat + * @return + */ + public static TransformSheet getTransformSheet(JsonObject jObject, @Nullable OpenMatrix4f invLocalTransform, boolean rootCorrection, TransformFormat transformFormat) throws AssetLoadingException, JsonParseException { + JsonArray timeArray = jObject.getAsJsonArray("time"); + JsonArray transformArray = jObject.getAsJsonArray("transform"); + + if (timeArray.size() != transformArray.size()) { + throw new AssetLoadingException( + "Can't read transform sheet: the size of timestamp and transform array is different." + + "timestamp array size: " + timeArray.size() + ", transform array size: " + transformArray.size() + ); + } + + int timesCount = timeArray.size(); + List keyframeList = Lists.newArrayList(); + + for (int i = 0; i < timesCount; i++) { + float timeStamp = timeArray.get(i).getAsFloat(); + + if (timeStamp < 0.0F) { + continue; + } + + // WORKAROUND: The case when transform format is wrongly specified! + if (transformFormat == TransformFormat.ATTRIBUTES && transformArray.get(i).isJsonArray()) { + transformFormat = TransformFormat.MATRIX; + } else if (transformFormat == TransformFormat.MATRIX && transformArray.get(i).isJsonObject()) { + transformFormat = TransformFormat.ATTRIBUTES; + } + + switch (transformFormat) { + case MATRIX -> { + JsonArray matrixArray = transformArray.get(i).getAsJsonArray(); + float[] matrixElements = new float[16]; + + for (int j = 0; j < 16; j++) { + matrixElements[j] = matrixArray.get(j).getAsFloat(); + } + + OpenMatrix4f matrix = OpenMatrix4f.load(null, matrixElements); + matrix.transpose(); + + if (rootCorrection) { + matrix.mulFront(BLENDER_TO_MINECRAFT_COORD); + } + + matrix.mulFront(invLocalTransform); + + JointTransform transform = JointTransform.fromMatrix(matrix); + transform.rotation().normalize(); + keyframeList.add(new Keyframe(timeStamp, transform)); + } + case ATTRIBUTES -> { + JsonObject transformObject = transformArray.get(i).getAsJsonObject(); + JsonArray locArray = transformObject.get("loc").getAsJsonArray(); + JsonArray rotArray = transformObject.get("rot").getAsJsonArray(); + JsonArray scaArray = transformObject.get("sca").getAsJsonArray(); + JointTransform transform + = JointTransform.fromPrimitives( + locArray.get(0).getAsFloat() + , locArray.get(1).getAsFloat() + , locArray.get(2).getAsFloat() + , -rotArray.get(1).getAsFloat() + , -rotArray.get(2).getAsFloat() + , -rotArray.get(3).getAsFloat() + , rotArray.get(0).getAsFloat() + , scaArray.get(0).getAsFloat() + , scaArray.get(1).getAsFloat() + , scaArray.get(2).getAsFloat() + ); + + keyframeList.add(new Keyframe(timeStamp, transform)); + } + } + } + + TransformSheet sheet = new TransformSheet(keyframeList); + + return sheet; + } + + /** + * Determines how the transform is expressed in json + * + * {@link TransformFormat#MATRIX} be like, + * [0, 1, 2, ..., 15] + * + * {@link TransformFormat#ATTRIBUTES} be like, + * { + * "loc": [0, 0, 0], + * "rot": [0, 0, 0, 1], + * "sca": [1, 1, 1], + * } + */ + public enum TransformFormat { + MATRIX, ATTRIBUTES + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/asset/SelfAccessor.java b/src/main/java/com/tiedup/remake/rig/asset/SelfAccessor.java new file mode 100644 index 0000000..2891148 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/asset/SelfAccessor.java @@ -0,0 +1,25 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.asset; + +import net.minecraft.resources.ResourceLocation; + +public record SelfAccessor(ResourceLocation registryName, A asset) implements AssetAccessor { + public static SelfAccessor create(ResourceLocation registryName, A asset) { + return new SelfAccessor<> (registryName, asset); + } + + @Override + public A get() { + return this.asset; + } + + @Override + public boolean inRegistry() { + return false; + } +} diff --git a/src/main/java/com/tiedup/remake/rig/cloth/AbstractSimulator.java b/src/main/java/com/tiedup/remake/rig/cloth/AbstractSimulator.java new file mode 100644 index 0000000..5a6726c --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/cloth/AbstractSimulator.java @@ -0,0 +1,135 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.cloth; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BooleanSupplier; + +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.collect.Maps; + +import yesman.epicfight.api.physics.PhysicsSimulator; +import yesman.epicfight.api.physics.SimulationObject; +import yesman.epicfight.api.physics.SimulationObject.SimulationObjectBuilder; +import yesman.epicfight.api.physics.SimulationProvider; + +public abstract class AbstractSimulator, O, SO extends SimulationObject> implements PhysicsSimulator { + protected Map simulationObjects = Maps.newHashMap(); + + @Override + public void tick(O simObject) { + this.simulationObjects.values().removeIf((keyWrapper) -> { + if (keyWrapper.isRunning()) { + if (!keyWrapper.runWhen.getAsBoolean()) { + keyWrapper.stopRunning(); + + if (!keyWrapper.permanent) { + return true; + } + } + } else { + if (keyWrapper.runWhen.getAsBoolean()) { + keyWrapper.startRunning(simObject); + } + } + + return false; + }); + } + + /** + * Add a simulation object and run. Remove when @Param until returns false + */ + @Override + public void runUntil(KEY key, PV provider, B builder, BooleanSupplier until) { + this.simulationObjects.put(key, new ObjectWrapper(provider, until, false, builder)); + } + + /** + * Add an undeleted simulation object. Run simulation when @Param when returns true + */ + @Override + public void runWhen(KEY key, PV provider, B builder, BooleanSupplier when) { + this.simulationObjects.put(key, new ObjectWrapper(provider, when, true, builder)); + } + + /** + * Stop simulation + */ + @Override + public void stop(KEY key) { + this.simulationObjects.remove(key); + } + + /** + * Restart with the same condition but with another provider + */ + @Override + public void restart(KEY key) { + ObjectWrapper kwrap = this.simulationObjects.get(key); + + if (kwrap != null) { + this.stop(key); + this.simulationObjects.put(key, new ObjectWrapper(kwrap.provider, kwrap.runWhen, kwrap.permanent, kwrap.builder)); + } + } + + @Override + public boolean isRunning(KEY key) { + return this.simulationObjects.containsKey(key) ? this.simulationObjects.get(key).isRunning() : false; + } + + @Override + public Optional getRunningObject(KEY key) { + if (!this.simulationObjects.containsKey(key)) { + return Optional.empty(); + } + + return Optional.ofNullable(this.simulationObjects.get(key).simulationObject); + } + + public List> getAllRunningObjects() { + return this.simulationObjects.entrySet().stream().filter((entry) -> entry.getValue().isRunning()).map((entry) -> Pair.of(entry.getKey(), entry.getValue().simulationObject)).toList(); + } + + protected class ObjectWrapper { + final PV provider; + final B builder; + final BooleanSupplier runWhen; + final boolean permanent; + + SO simulationObject; + boolean isRunning; + + ObjectWrapper(PV key, BooleanSupplier runWhen, boolean permanent, B builder) { + this.provider = key; + this.runWhen = runWhen; + this.permanent = permanent; + this.builder = builder; + } + + public void startRunning(O simObject) { + this.simulationObject = this.provider.createSimulationData(this.provider, simObject, this.builder); + + if (this.simulationObject != null) { + this.isRunning = true; + } + } + + public void stopRunning() { + this.isRunning = false; + this.simulationObject = null; + } + + public boolean isRunning() { + return this.isRunning; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/cloth/ClothColliderPresets.java b/src/main/java/com/tiedup/remake/rig/cloth/ClothColliderPresets.java new file mode 100644 index 0000000..406c6de --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/cloth/ClothColliderPresets.java @@ -0,0 +1,68 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.cloth; + +import java.util.List; +import java.util.function.Function; + +import com.google.common.collect.ImmutableList; +import com.mojang.datafixers.util.Pair; + +import com.tiedup.remake.rig.math.OpenMatrix4f; + +/** + * 0: Root, + * 1: Thigh_R, + * 2: "Leg_R", + * 3: "Knee_R", + * 4: "Thigh_L", + * 5: "Leg_L", + * 6: "Knee_L", + * 7: "Torso", + * 8: "Chest", + * 9: "Head", + * 10: "Shoulder_R", + * 11: "Arm_R", + * 12: "Hand_R", + * 13: "Tool_R", + * 14: "Elbow_R", + * 15: "Shoulder_L", + * 16: "Arm_L", + * 17: "Hand_L", + * 18: "Tool_L", + * 19: "Elbow_L" +**/ + +public class ClothColliderPresets { + public static final List, ClothSimulator.ClothOBBCollider>> BIPED_SLIM = ImmutableList., ClothSimulator.ClothOBBCollider>>builder() + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[1], new ClothSimulator.ClothOBBCollider(0.125D, 0.24D, 0.125D, 0.0D, 0.22D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[2], new ClothSimulator.ClothOBBCollider(0.125D, 0.1875D, 0.125D, 0.0D, 0.1875D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[4], new ClothSimulator.ClothOBBCollider(0.125D, 0.24D, 0.125D, 0.0D, 0.22D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[5], new ClothSimulator.ClothOBBCollider(0.125D, 0.1875D, 0.125D, 0.0D, 0.1875D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[7], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.13D, 0.0D, 0.125D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[8], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.13D, 0.0D, 0.3D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[9], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.25D, 0.0D, 0.2D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[11], new ClothSimulator.ClothOBBCollider(0.12D, 0.24D, 0.125D, -0.05D, 0.14D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[12], new ClothSimulator.ClothOBBCollider(0.12D, 0.1875D, 0.125D, -0.05D, 0.14D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[16], new ClothSimulator.ClothOBBCollider(0.12D, 0.24D, 0.125D, 0.05D, 0.14D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[17], new ClothSimulator.ClothOBBCollider(0.12D, 0.1875D, 0.125D, 0.05D, 0.14D, 0.0D))) + .build(); + + public static final List, ClothSimulator.ClothOBBCollider>> BIPED = ImmutableList., ClothSimulator.ClothOBBCollider>>builder() + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[1], new ClothSimulator.ClothOBBCollider(0.125D, 0.24D, 0.125D, 0.0D, 0.22D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[2], new ClothSimulator.ClothOBBCollider(0.125D, 0.1875D, 0.125D, 0.0D, 0.1875D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[4], new ClothSimulator.ClothOBBCollider(0.125D, 0.24D, 0.125D, 0.0D, 0.22D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[5], new ClothSimulator.ClothOBBCollider(0.125D, 0.1875D, 0.125D, 0.0D, 0.1875D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[7], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.13D, 0.0D, 0.125D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[8], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.13D, 0.0D, 0.3D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[9], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.25D, 0.0D, 0.2D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[11], new ClothSimulator.ClothOBBCollider(0.13D, 0.24D, 0.13D, -0.0D, 0.14D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[12], new ClothSimulator.ClothOBBCollider(0.13D, 0.1875D, 0.13D, -0.0D, 0.14D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[16], new ClothSimulator.ClothOBBCollider(0.13D, 0.24D, 0.13D, 0.0D, 0.14D, 0.0D))) + .add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[17], new ClothSimulator.ClothOBBCollider(0.13D, 0.1875D, 0.13D, 0.0D, 0.14D, 0.0D))) + .build(); +} diff --git a/src/main/java/com/tiedup/remake/rig/cloth/ClothSimulatable.java b/src/main/java/com/tiedup/remake/rig/cloth/ClothSimulatable.java new file mode 100644 index 0000000..3086571 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/cloth/ClothSimulatable.java @@ -0,0 +1,37 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.cloth; + +import javax.annotation.Nullable; + +import net.minecraft.world.phys.Vec3; +import com.tiedup.remake.rig.anim.Animator; +import com.tiedup.remake.rig.armature.Armature; +import yesman.epicfight.api.physics.SimulatableObject; + +public interface ClothSimulatable extends SimulatableObject { + @Nullable + Armature getArmature(); + + @Nullable + Animator getSimulatableAnimator(); + + boolean invalid(); + public Vec3 getObjectVelocity(); + public float getYRot(); + public float getYRotO(); + + // Cloth object requires providing location info for 2 steps before for accurate continuous collide detection. + public Vec3 getAccurateCloakLocation(float partialFrame); + public Vec3 getAccuratePartialLocation(float partialFrame); + public float getAccurateYRot(float partialFrame); + public float getYRotDelta(float partialFrame); + public float getScale(); + public float getGravity(); + + ClothSimulator getClothSimulator(); +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/cloth/ClothSimulator.java b/src/main/java/com/tiedup/remake/rig/cloth/ClothSimulator.java new file mode 100644 index 0000000..9e0844b --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/cloth/ClothSimulator.java @@ -0,0 +1,1931 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.cloth; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.annotation.Nullable; + +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.joml.Vector4f; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.datafixers.util.Pair; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import com.tiedup.remake.rig.armature.Joint; +import com.tiedup.remake.rig.mesh.CompositeMesh; +import com.tiedup.remake.rig.mesh.Mesh; +import com.tiedup.remake.rig.mesh.MeshPart; +import com.tiedup.remake.rig.mesh.SoftBodyTranslatable; +import com.tiedup.remake.rig.mesh.VertexBuilder; +import com.tiedup.remake.rig.cloth.AbstractSimulator; +import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObjectBuilder; +import yesman.epicfight.api.collider.OBBCollider; +import com.tiedup.remake.rig.armature.Armature; +import yesman.epicfight.api.physics.SimulationObject; +import com.tiedup.remake.rig.math.MathUtils; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.main.EpicFightSharedConstants; + +/** + * Referred to Matthias Müller's Ten minuates physics tutorial video number 14, 15 + * + * https://matthias-research.github.io/pages/tenMinutePhysics/index.html + * + * https://www.youtube.com/@TenMinutePhysics + **/ +public class ClothSimulator extends AbstractSimulator { + public static final ResourceLocation PLAYER_CLOAK = EpicFightMod.identifier("ingame_cloak"); + public static final ResourceLocation MODELPREVIEWER_CLOAK = EpicFightMod.identifier("previewer_cloak"); + private static final float SPATIAL_HASH_SPACING = 0.05F; + + public static class ClothObjectBuilder extends SimulationObject.SimulationObjectBuilder { + List, ClothSimulator.ClothOBBCollider>> clothColliders = Lists.newArrayList(); + Joint joint; + + public ClothObjectBuilder addEntry(Function obbTransformer, ClothOBBCollider clothOBBCollider) { + this.clothColliders.add(Pair.of(obbTransformer, clothOBBCollider)); + return this; + } + + public ClothObjectBuilder putAll(List, ClothSimulator.ClothOBBCollider>> clothOBBColliders) { + this.clothColliders.addAll(clothOBBColliders); + return this; + } + + public ClothObjectBuilder parentJoint(Joint joint) { + this.joint = joint; + return this; + } + + public static ClothObjectBuilder create() { + return new ClothObjectBuilder(); + } + } + + // Developer configurations + private static boolean DRAW_MESH_COLLIDERS = false; + private static boolean DRAW_NORMAL_OFFSET = true; + private static boolean DRAW_OUTLINES = false; + + public static void drawMeshColliders(boolean flag) { + if (!EpicFightSharedConstants.IS_DEV_ENV) { + throw new IllegalStateException("Can't switch developer configuration in product environment."); + } + + DRAW_MESH_COLLIDERS = flag; + } + + public static void drawNormalOffset(boolean flag) { + if (!EpicFightSharedConstants.IS_DEV_ENV) { + throw new IllegalStateException("Can't switch developer configuration in product environment."); + } + + DRAW_NORMAL_OFFSET = flag; + } + + public static void drawOutlines(boolean flag) { + if (!EpicFightSharedConstants.IS_DEV_ENV) { + throw new IllegalStateException("Can't switch developer configuration in product environment."); + } + + DRAW_OUTLINES = flag; + } + + public static class ClothObject implements SimulationObject, Mesh { + private final SoftBodyTranslatable provider; + private final Map parts; + + private final Map particles; + private final Map normalOffsetParticles; + private final List> particleNormals; + + private final Quaternionf rotationO = new Quaternionf(); + private final Vec3f centrifugalO = new Vec3f(); + + @Nullable + protected List, ClothSimulator.ClothOBBCollider>> clothColliders; + protected final Joint parentJoint; + + //Storage vectors + private static final Vec3f TRASNFORMED = new Vec3f(); + private static final Vector4f POSITION = new Vector4f(); + private static final Vector3f NORMAL = new Vector3f(); + + public ClothObject(ClothObjectBuilder builder, SoftBodyTranslatable provider, Map parts, float[] positions) { + this.clothColliders = builder.clothColliders; + this.parentJoint = builder.joint; + + this.provider = provider; + this.particles = Maps.newHashMap(); + this.normalOffsetParticles = Maps.newHashMap(); + this.particleNormals = Lists.newArrayList(); + + for (int i = 0; i < positions.length / 3; i++) { + this.particleNormals.add(Maps.newHashMap()); + } + + for (Map.Entry meshPart : parts.entrySet()) { + for (VertexBuilder vb : meshPart.getValue().getVertices()) { + Map posNormals = this.particleNormals.get(vb.position); + + if (!posNormals.containsKey(vb.normal)) { + provider.getOriginalMesh().getVertexNormal(vb.normal, NORMAL); + posNormals.put(vb.normal, new Vec3f(NORMAL.x, NORMAL.y, NORMAL.z)); + } + } + } + + ImmutableMap.Builder partBuilder = ImmutableMap.builder(); + + for (Map.Entry entry : provider.getSoftBodySimulationInfo().entrySet()) { + partBuilder.put(entry.getKey(), new ClothPart(entry.getValue(), positions)); + } + + this.parts = partBuilder.build(); + } + + private ClothObject(ClothObject copyTarget) { + this.provider = copyTarget.provider; + this.parts = copyTarget.parts; + + this.particles = new HashMap<> (); + this.normalOffsetParticles = new HashMap<> (); + + for (Map.Entry entry : copyTarget.particles.entrySet()) { + this.particles.put(entry.getKey(), entry.getValue().copy()); + } + + for (Map.Entry entry : copyTarget.normalOffsetParticles.entrySet()) { + this.normalOffsetParticles.put(entry.getKey(), entry.getValue().copy()); + } + + for (Map.Entry entry : copyTarget.normalOffsetParticles.entrySet()) { + this.normalOffsetParticles.put(entry.getKey(), entry.getValue().copy()); + } + + this.particleNormals = ImmutableList.copyOf(copyTarget.particleNormals); + this.parentJoint = copyTarget.parentJoint; + } + + public ClothObject captureMyself() { + return new ClothObject(this); + } + + private static final int SUB_STEPS = 6; + private static final Vec3f EXTERNAL_FORCE = new Vec3f(); + private static final Vec3f OFFSET = new Vec3f(); + private static final Vec3f CENTRIFUGAL = new Vec3f(); + private static final Vec3f CIRCULAR = new Vec3f(); + + private static final OpenMatrix4f[] BOUND_ANIMATION_TRANSFORM = OpenMatrix4f.allocateMatrixArray(EpicFightSharedConstants.MAX_JOINTS); + private static final OpenMatrix4f COLLIDER_TRANSFORM = new OpenMatrix4f(); + private static final OpenMatrix4f TO_CENTRIFUGAL = new OpenMatrix4f(); + private static final OpenMatrix4f OBJECT_TRANSFORM = new OpenMatrix4f(); + private static final OpenMatrix4f INVERTED = new OpenMatrix4f(); + private static final Quaternionf ROTATOR = new Quaternionf(); + + /** + * This method needs be called before drawing simulated cloth + */ + public void tick(ClothSimulatable simulatableObj, Function colliderTransformGetter, float partialTick, @Nullable Armature armature, @Nullable OpenMatrix4f[] poses) { + // Configure developer options + //drawMeshColliders(true); + //drawNormalOffset(false); + //drawOutlines(true); + + // Revert + //drawMeshColliders(false); + //drawNormalOffset(true); + //drawOutlines(false); + + if (!Minecraft.getInstance().isPaused()) { + boolean skinned = poses != null && armature != null; + + for (int j = 0; j < armature.getJointNumber(); j++) { + if (skinned) { + BOUND_ANIMATION_TRANSFORM[j].load(poses[j]); + BOUND_ANIMATION_TRANSFORM[j].mulBack(armature.searchJointById(j).getToOrigin()); + BOUND_ANIMATION_TRANSFORM[j].removeScale(); + } + } + + float deltaFrameTime = Minecraft.getInstance().getDeltaFrameTime(); + float subStebInvert = 1.0F / SUB_STEPS; + float subSteppingDeltaTime = deltaFrameTime * subStebInvert; + float gravity = simulatableObj.getGravity() * subSteppingDeltaTime * EpicFightSharedConstants.A_TICK; + + // Update circular force + float yRot = Mth.wrapDegrees(Mth.rotLerp(partialTick, Mth.wrapDegrees(simulatableObj.getYRotO()), Mth.wrapDegrees(simulatableObj.getYRot()))); + + TO_CENTRIFUGAL.load(BOUND_ANIMATION_TRANSFORM[this.parentJoint.getId()]); + TO_CENTRIFUGAL.mulFront(OpenMatrix4f.createRotatorDeg(-yRot + 180.0F, Vec3f.Y_AXIS)); + TO_CENTRIFUGAL.toQuaternion(ROTATOR); + + Vec3 velocity = simulatableObj.getObjectVelocity(); + float delta = MathUtils.wrapRadian(MathUtils.getAngleBetween(this.rotationO, ROTATOR)); + float speed = Math.min((float)velocity.length() * deltaFrameTime, 0.2F); + float rotationForce = Math.abs(delta); + + this.rotationO.set(ROTATOR); + + OpenMatrix4f.transform3v(TO_CENTRIFUGAL, Vec3f.Z_AXIS, CENTRIFUGAL); + int deltaSign = Math.abs(delta) < 0.02D ? 0 : MathUtils.getSign(delta); + + if (deltaSign == 0) { + CIRCULAR.set(Vec3f.ZERO); + } else { + Vec3f.sub(CENTRIFUGAL, this.centrifugalO, CIRCULAR); + CIRCULAR.normalize(); + } + + this.centrifugalO.set(CENTRIFUGAL); + + CENTRIFUGAL.scale(rotationForce * (1.0F + speed * 50.0F)); + CIRCULAR.scale(rotationForce * (1.0F + speed * 50.0F)); + velocity = velocity.scale(rotationForce); + + Vec3f.add(CIRCULAR, CENTRIFUGAL, EXTERNAL_FORCE); + EXTERNAL_FORCE.add(velocity); + + // Reset normal vectors + this.particleNormals.forEach((poseNormals) -> poseNormals.values().forEach((vec3f) -> vec3f.set(0.0F, 0.0F, 0.0F))); + + Vec3 pos = simulatableObj.getAccuratePartialLocation(partialTick); + float yRotLerp = simulatableObj.getAccurateYRot(partialTick); + OpenMatrix4f objectTransform = OpenMatrix4f.ofTranslation((float)pos.x, (float)pos.y, (float)pos.z, OBJECT_TRANSFORM).rotateDeg(180.0F - yRotLerp, Vec3f.Y_AXIS); + OpenMatrix4f.invert(objectTransform, INVERTED); + + for (ClothPart part : this.parts.values()) { + part.tick(objectTransform, EXTERNAL_FORCE, skinned ? BOUND_ANIMATION_TRANSFORM : null); + } + + for (int i = 0; i < SUB_STEPS; i++) { + float substepPartialTick = partialTick - deltaFrameTime + subSteppingDeltaTime * (i + 1); + + if (this.clothColliders != null) { + simulatableObj.getArmature().setPose(simulatableObj.getSimulatableAnimator().getPose(Mth.clamp(substepPartialTick, 0.0F, 1.0F))); + OpenMatrix4f colliderTransform = colliderTransformGetter.apply(substepPartialTick); + + for (Pair, ClothSimulator.ClothOBBCollider> entry : this.clothColliders) { + entry.getSecond().transform(OpenMatrix4f.mul(colliderTransform, entry.getFirst().apply(simulatableObj), COLLIDER_TRANSFORM)); + } + } + + for (ClothPart part : this.parts.values()) { + part.substepTick(gravity, subSteppingDeltaTime, i + 1, this.clothColliders); + } + } + } + + this.updateNormal(false); + + // Update normals & offset particles + if (!this.normalOffsetParticles.isEmpty()) { + for (ClothPart.OffsetParticle offsetParticle : this.normalOffsetParticles.values()) { + // Update offset positions + Particle rootParticle = offsetParticle.rootParticle(); + Map rootNormalMap = this.particleNormals.get(rootParticle.meshVertexId); + OFFSET.set(0.0F, 0.0F, 0.0F); + + for (Integer normIdx : offsetParticle.positionNormalMembers()) { + OFFSET.add(rootNormalMap.get(normIdx).normalize()); + } + + OFFSET.scale(offsetParticle.length / OFFSET.length()); + + offsetParticle.position.set( + rootParticle.position.x - OFFSET.x + , rootParticle.position.y - OFFSET.y + , rootParticle.position.z - OFFSET.z + ); + } + } + + this.updateNormal(true); + this.captureModelPosition(INVERTED); + } + + private static final Vec3f TO_P2 = new Vec3f(); + private static final Vec3f TO_P3 = new Vec3f(); + private static final Vec3f CROSS = new Vec3f(); + + // Calculate vertex normals + private void updateNormal(boolean updateOffsetParticles) { + SoftBodyTranslatable softBodyMesh = this.provider; + + for (MeshPart modelPart : softBodyMesh.getOriginalMesh().getAllParts()) { + for (int i = 0; i < modelPart.getVertices().size() / 3; i++) { + VertexBuilder triP1 = modelPart.getVertices().get(i * 3); + VertexBuilder triP2 = modelPart.getVertices().get(i * 3 + 1); + VertexBuilder triP3 = modelPart.getVertices().get(i * 3 + 2); + + if (!this.particles.containsKey(triP1.position) || !this.particles.containsKey(triP2.position) || !this.particles.containsKey(triP3.position)) { + if (!updateOffsetParticles) { + continue; + } + } else { + if (updateOffsetParticles) { + continue; + } + } + + Vec3f p1Pos = this.getParticlePosition(triP1.position); + Vec3f p2Pos = this.getParticlePosition(triP2.position); + Vec3f p3Pos = this.getParticlePosition(triP3.position); + + Vec3f.cross(Vec3f.sub(p2Pos, p1Pos, TO_P2), Vec3f.sub(p3Pos, p1Pos, TO_P3), CROSS); + CROSS.normalize(); + + Map triP1Normals = particleNormals.get(triP1.position); + Map triP2Normals = particleNormals.get(triP2.position); + Map triP3Normals = particleNormals.get(triP3.position); + + triP1Normals.get(triP1.normal).add(CROSS); + triP2Normals.get(triP2.normal).add(CROSS); + triP3Normals.get(triP3.normal).add(CROSS); + } + } + } + + private static final Vec3f SCALE = new Vec3f(); + + public void scaleFromPose(PoseStack poseStack, OpenMatrix4f[] poses) { + OpenMatrix4f poseMat = poses[this.parentJoint.getId()]; + poseMat.toScaleVector(SCALE); + + poseStack.translate(poseMat.m30, poseMat.m31, poseMat.m32); + poseStack.scale(SCALE.x, SCALE.y, SCALE.z); + poseStack.translate(-poseMat.m30, -poseMat.m31, -poseMat.m32); + } + + @Override + public void draw(PoseStack poseStack, VertexConsumer bufferBuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) { + if (DRAW_OUTLINES) { + this.drawOutline(poseStack, Minecraft.getInstance().renderBuffers().bufferSource().getBuffer(RenderType.lines()), Mesh.DrawingFunction.POSITION_COLOR_NORMAL, r, g, b, a); + //part.drawNormals(poseStack, Minecraft.getInstance().renderBuffers().bufferSource().getBuffer(RenderType.lines()), Mesh.DrawingFunction.POSITION_COLOR_NORMAL, r, g, b, a); + } else { + this.drawParts(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay); + } + + if (this.provider instanceof CompositeMesh compositeMesh) { + poseStack.popPose(); + compositeMesh.getStaticMesh().draw(poseStack, bufferBuilder, drawingFunction, packedLight, 1.0F, 1.0F, 1.0F, 1.0F, overlay); + poseStack.pushPose(); + } + } + + private static final Vector3f SCALER = new Vector3f(); + + @Override + public void drawPosed(PoseStack poseStack, VertexConsumer bufferBuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, Armature armature, OpenMatrix4f[] poses) { + if (DRAW_OUTLINES) { + this.drawOutline(poseStack, Minecraft.getInstance().renderBuffers().bufferSource().getBuffer(RenderType.lines()), Mesh.DrawingFunction.POSITION_COLOR_NORMAL, r, g, b, a); + //part.drawNormals(poseStack, Minecraft.getInstance().renderBuffers().bufferSource().getBuffer(RenderType.lines()), Mesh.DrawingFunction.POSITION_COLOR_NORMAL, r, g, b, a); + } else { + this.drawParts(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay); + } + + if (DRAW_MESH_COLLIDERS && this.clothColliders != null) { + for (Pair, ClothSimulator.ClothOBBCollider> entry : this.clothColliders) { + entry.getSecond().draw(poseStack, Minecraft.getInstance().renderBuffers().bufferSource(), 0xFFFFFFFF); + } + } + + // Remove entity inverted world translation while keeping the scale + poseStack.last().pose().getScale(SCALER); + float scaleX = SCALER.x; + float scaleY = SCALER.y; + float scaleZ = SCALER.z; + + poseStack.popPose(); + poseStack.last().pose().getScale(SCALER); + float xDiv = scaleX / SCALER.x; + float yDiv = scaleY / SCALER.y; + float zDiv = scaleZ / SCALER.z; + + poseStack.scale(xDiv, yDiv, zDiv); + + if (this.provider instanceof CompositeMesh compositeMesh) { + compositeMesh.getStaticMesh().drawPosed(poseStack, bufferBuilder, drawingFunction, packedLight, 1.0F, 1.0F, 1.0F, 1.0F, overlay, armature, poses); + } + + poseStack.pushPose(); + } + + public Vec3f getParticlePosition(int idx) { + if (this.particles.containsKey(idx)) { + return this.particles.get(idx).position; + } else { + return this.normalOffsetParticles.get(idx).position; + } + } + + private void captureModelPosition(OpenMatrix4f objectTranslformInv) { + for (Particle p : this.particles.values()) { + OpenMatrix4f.transform3v(objectTranslformInv, p.position, p.modelPosition); + } + } + + public void drawParts(PoseStack poseStack, VertexConsumer bufferBuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) { + SoftBodyTranslatable softBodyMesh = ClothObject.this.provider; + float[] uvs = softBodyMesh.getOriginalMesh().uvs(); + + for (MeshPart meshPart : softBodyMesh.getOriginalMesh().getAllParts()) { + if (meshPart.isHidden()) { + continue; + } + + Vector4f color = meshPart.getColor(r, g, b, a); + Matrix4f matrix4f = poseStack.last().pose(); + Matrix3f matrix3f = poseStack.last().normal(); + + for (int i = 0; i < meshPart.getVertices().size(); i++) { + if (!DRAW_NORMAL_OFFSET && i % 3 == 0) { + if (i + 1 == meshPart.getVertices().size() || i + 2 == meshPart.getVertices().size()) { + + } else { + VertexBuilder v1 = meshPart.getVertices().get(i); + VertexBuilder v2 = meshPart.getVertices().get(i + 1); + VertexBuilder v3 = meshPart.getVertices().get(i + 2); + + if ((!this.particles.containsKey(v1.position) || !this.particles.containsKey(v2.position) || !this.particles.containsKey(v3.position))) { + i += 2; + continue; + } + } + } + + VertexBuilder vb = meshPart.getVertices().get(i); + Vec3f particlePosition = this.getParticlePosition(vb.position); + Vec3f poseNormal = this.particleNormals.get(vb.position).get(vb.normal); + poseNormal.normalize(); + + POSITION.set(particlePosition.x, particlePosition.y, particlePosition.z); + NORMAL.set(poseNormal.x, poseNormal.y, poseNormal.z); + POSITION.mul(matrix4f); + NORMAL.mul(matrix3f); + + drawingFunction.draw(bufferBuilder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x(), NORMAL.y(), NORMAL.z(), packedLight, color.x, color.y, color.z, color.w, uvs[vb.uv * 2], uvs[vb.uv * 2 + 1], overlay); + } + } + } + + public void drawOutline(PoseStack poseStack, VertexConsumer builder, Mesh.DrawingFunction drawingFunction, float r, float g, float b, float a) { + SoftBodyTranslatable softBodyMesh = ClothObject.this.provider; + + for (MeshPart meshPart : softBodyMesh.getOriginalMesh().getAllParts()) { + if (meshPart.isHidden()) { + continue; + } + + Matrix4f matrix4f = poseStack.last().pose(); + Matrix3f matrix3f = poseStack.last().normal(); + + for (int i = 0; i < meshPart.getVertices().size() / 3; i++) { + VertexBuilder v1 = meshPart.getVertices().get(i * 3); + VertexBuilder v2 = meshPart.getVertices().get(i * 3 + 1); + VertexBuilder v3 = meshPart.getVertices().get(i * 3 + 2); + + if (!DRAW_NORMAL_OFFSET && (!this.particles.containsKey(v1.position) || !this.particles.containsKey(v2.position) || !this.particles.containsKey(v3.position))) { + continue; + } + + Vec3f pos1 = this.getParticlePosition(v1.position); + Vec3f pos2 = this.getParticlePosition(v2.position); + Vec3f pos3 = this.getParticlePosition(v3.position); + + POSITION.set(pos1.x, pos1.y, pos1.z); + NORMAL.set(pos2.x - pos1.x, pos2.x - pos1.x, pos2.x - pos1.x); + POSITION.mul(matrix4f); + NORMAL.mul(matrix3f); + drawingFunction.draw(builder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x(), NORMAL.y(), NORMAL.z(), -1, r, g, b, a, 0, 0, 0); + POSITION.set(pos2.x, pos2.y, pos2.z); + POSITION.mul(matrix4f); + drawingFunction.draw(builder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x(), NORMAL.y(), NORMAL.z(), -1, r, g, b, a, 0, 0, 0); + + POSITION.set(pos2.x, pos2.y, pos2.z); + NORMAL.set(pos3.x - pos2.x, pos3.x - pos2.x, pos3.x - pos2.x); + POSITION.mul(matrix4f); + NORMAL.mul(matrix3f); + drawingFunction.draw(builder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x(), NORMAL.y(), NORMAL.z(), -1, r, g, b, a, 0, 0, 0); + POSITION.set(pos3.x, pos3.y, pos3.z); + POSITION.mul(matrix4f); + drawingFunction.draw(builder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x(), NORMAL.y(), NORMAL.z(), -1, r, g, b, a, 0, 0, 0); + + POSITION.set(pos3.x, pos3.y, pos3.z); + NORMAL.set(pos1.x - pos3.x, pos1.x - pos3.x, pos1.x - pos3.x); + POSITION.mul(matrix4f); + NORMAL.mul(matrix3f); + drawingFunction.draw(builder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x(), NORMAL.y(), NORMAL.z(), -1, r, g, b, a, 0, 0, 0); + POSITION.set(pos1.x, pos1.y, pos1.z); + POSITION.mul(matrix4f); + drawingFunction.draw(builder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x(), NORMAL.y(), NORMAL.z(), -1, r, g, b, a, 0, 0, 0); + } + } + } + + public void drawNormals(PoseStack poseStack, VertexConsumer builder, Mesh.DrawingFunction drawingFunction, float r, float g, float b, float a) { + if (!this.normalOffsetParticles.isEmpty()) { + Matrix4f matrix4f = poseStack.last().pose(); + Matrix3f matrix3f = poseStack.last().normal(); + + for (ClothPart.OffsetParticle offsetParticle : this.normalOffsetParticles.values()) { + // Update offset positions + Particle rootParticle = offsetParticle.rootParticle(); + Map rootNormalMap = this.particleNormals.get(rootParticle.meshVertexId); + + if (rootNormalMap.size() < 2) { + continue; + } + + OFFSET.set(0.0F, 0.0F, 0.0F); + + for (Integer normIdx : offsetParticle.positionNormalMembers()) { + OFFSET.add(rootNormalMap.get(normIdx).normalize()); + } + + OFFSET.scale(offsetParticle.length / OFFSET.length()); + + Vec3f rootpos = this.getParticlePosition(rootParticle.meshVertexId); + + POSITION.set(rootpos.x, rootpos.y, rootpos.z); + NORMAL.set(-OFFSET.x, -OFFSET.x, -OFFSET.x); + POSITION.mul(matrix4f); + NORMAL.mul(matrix3f); + drawingFunction.draw(builder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x(), NORMAL.y(), NORMAL.z(), -1, r, g, b, a, 0, 0, 0); + POSITION.set(rootpos.x - OFFSET.x, rootpos.y - OFFSET.y, rootpos.z - OFFSET.z); + POSITION.mul(matrix4f); + drawingFunction.draw(builder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x(), NORMAL.y(), NORMAL.z(), -1, r, g, b, a, 0, 0, 0); + } + } + } + + @Override + public void initialize() { + } + + class Particle { + final Vec3f position; + final Vec3f modelPosition; + final Vec3f velocity = new Vec3f(); + + final float influence; + final float rootDistance; + final int meshVertexId; + boolean collided; + + Particle(Vec3f position, float influence, float rootDistance, int meshVertexId) { + this.position = position; + this.modelPosition = position.copy(); + this.influence = influence; + this.rootDistance = rootDistance; + this.meshVertexId = meshVertexId; + this.collided = false; + } + + Particle copy() { + return new Particle(this.position.copy(), this.influence, this.rootDistance, this.meshVertexId); + } + } + + public class ClothPart { + final List particleList; + final List constraints; + final Multimap spatialHash; + final float selfCollision; + final float particleMass; + final int hashTableSize; + + private static final Vec3f AVERAGE = new Vec3f(); + + ClothPart(SoftBodyTranslatable.ClothSimulationInfo clothInfo, float[] positions) { + this.particleList = Lists.newArrayList(); + ImmutableList.Builder constraintsBuilder = ImmutableList.builder(); + + this.selfCollision = clothInfo.selfCollision(); + this.particleMass = clothInfo.particleMass(); + + /** + * Add particles + */ + for (int i = 0; i < clothInfo.particles().length / 2; i++) { + int positionIndex = clothInfo.particles()[i * 2]; + int weightIndex = clothInfo.particles()[i * 2 + 1]; + float influence = clothInfo.weights()[weightIndex]; + float rootDistance = clothInfo.rootDistance()[i]; + float x = positions[positionIndex * 3]; + float y = positions[positionIndex * 3 + 1]; + float z = positions[positionIndex * 3 + 2]; + + Particle particle = new Particle(new Vec3f(x, y, z), influence, rootDistance, positionIndex); + ClothObject.this.particles.put(positionIndex, particle); + this.particleList.add(particle); + } + + this.hashTableSize = this.particleList.size() * 2; + this.spatialHash = HashMultimap.create(this.hashTableSize, 2); + int idx = 0; + + /** + * Add constraints + */ + for (int[] constraints : clothInfo.constraints()) { + float compliance = clothInfo.compliances()[idx]; + ConstraintType constraintType = clothInfo.constraintTypes()[idx]; + List constraintList; + idx++; + + switch(constraintType) { + case STRETCHING -> { + constraintList = new ArrayList<> (constraints.length / 2); + + for (int i = 0; i < constraints.length / 2; i++) { + int idx1 = constraints[i * 2]; + int idx2 = constraints[i * 2 + 1]; + + constraintList.add(new StretchingConstraint(ClothObject.this.particles.get(idx1), ClothObject.this.particles.get(idx2))); + } + + constraintsBuilder.add(new ConstraintList(compliance, constraintType, constraintList)); + } + case SHAPING -> { + constraintList = new ArrayList<> (constraints.length / 2); + + for (int i = 0; i < constraints.length / 2; i++) { + int idx1 = constraints[i * 2]; + int idx2 = constraints[i * 2 + 1]; + + constraintList.add(new ShapingConstraint(ClothObject.this.particles.get(idx1), ClothObject.this.particles.get(idx2))); + } + + constraintsBuilder.add(new ConstraintList(compliance, constraintType, constraintList)); + } + case BENDING -> { + constraintList = new ArrayList<> (constraints.length / 4); + + for (int i = 0; i < constraints.length / 4; i++) { + int idx1 = constraints[i * 4]; + int idx2 = constraints[i * 4 + 1]; + int idx3 = constraints[i * 4 + 2]; + int idx4 = constraints[i * 4 + 3]; + + constraintList.add(new BendingConstraint(ClothObject.this.particles.get(idx1), ClothObject.this.particles.get(idx2), ClothObject.this.particles.get(idx3), ClothObject.this.particles.get(idx4))); + } + + constraintsBuilder.add(new ConstraintList(compliance, constraintType, constraintList)); + } + case VOLUME -> { + constraintList = new ArrayList<> (constraints.length / 4); + + for (int i = 0; i < constraints.length / 4; i++) { + int idx1 = constraints[i * 4]; + int idx2 = constraints[i * 4 + 1]; + int idx3 = constraints[i * 4 + 2]; + int idx4 = constraints[i * 4 + 3]; + + constraintList.add(new VolumeConstraint(ClothObject.this.particles.get(idx1), ClothObject.this.particles.get(idx2), ClothObject.this.particles.get(idx3), ClothObject.this.particles.get(idx4))); + } + + constraintsBuilder.add(new ConstraintList(compliance, constraintType, constraintList)); + } + } + } + + this.constraints = constraintsBuilder.build(); + + /** + * Setup normal offsets + */ + if (clothInfo.normalOffsetMapping() != null) { + for (int i = 0; i < clothInfo.normalOffsetMapping().length / 2; i++) { + int rootParticle = clothInfo.normalOffsetMapping()[i * 2]; + int offsetParticleIdx = clothInfo.normalOffsetMapping()[i * 2 + 1]; + Vec3f offsetDirection = new Vec3f( positions[offsetParticleIdx * 3] - positions[rootParticle * 3] + , positions[offsetParticleIdx * 3 + 1] - positions[rootParticle * 3 + 1] + , positions[offsetParticleIdx * 3 + 2] - positions[rootParticle * 3 + 2]); + + List positionNormalMembers = Lists.newArrayList(); + List inverseNormals = Lists.newArrayList(); + OffsetParticle offsetParticle = new OffsetParticle(offsetParticleIdx, offsetDirection.length(), ClothObject.this.particles.get(rootParticle), new Vec3f(), positionNormalMembers, inverseNormals); + offsetDirection.normalize(); + + Map rootNormalMap = particleNormals.get(rootParticle); + List rootNormals = new ArrayList<> (rootNormalMap.values()); + List> normalSubsets = new ArrayList<> (MathUtils.getSubset(IntStream.rangeClosed(0, rootNormals.size() - 1).boxed().toList())); + int candidate = -1; + int loopIdx = 0; + float maxDot = -10000.0F; + + for (Set subset : normalSubsets) { + Set rootNormal = subset.stream().map((normIdx) -> rootNormals.get(normIdx)).collect(Collectors.toSet()); + Vec3f.average(rootNormal, AVERAGE); + AVERAGE.scale(-1.0F); + AVERAGE.normalize(); + + float dot = Vec3f.dot(offsetDirection, AVERAGE); + if (maxDot < dot) { + maxDot = dot; + candidate = loopIdx; + } + + loopIdx++; + } + + normalSubsets.get(candidate).forEach((orderIdx) -> { + int iterCount = 0; + Iterator> iter = rootNormalMap.entrySet().iterator(); + + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + + if (orderIdx == iterCount) { + positionNormalMembers.add(entry.getKey()); + break; + } + + iterCount++; + } + }); + + normalOffsetParticles.put(offsetParticleIdx, offsetParticle); + + for (Vec3f normal : particleNormals.get(offsetParticleIdx).values()) { + int leastDotIdx = MathUtils.getLeastAngleVectorIdx(normal, rootNormals.toArray(new Vec3f[0])); + int iterCount = 0; + Iterator> iter = rootNormalMap.entrySet().iterator(); + + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + + if (leastDotIdx == iterCount) { + inverseNormals.add(entry.getKey()); + break; + } + + iterCount++; + } + } + } + } + } + + public void buildSpatialHash() { + this.spatialHash.clear(); + + // Create spatial hash map + for (Particle p : this.particleList) { + int hash = this.getHash(p.position.x, p.position.y, p.position.z); + this.spatialHash.put(hash, p); + } + } + + // Storage vectors + private static final Vec3f VEC3F = new Vec3f(); + private static final Vector4f POSITION = new Vector4f(0.0F, 0.0F, 0.0F, 1.0F); + private static final Vec3f DIFF = new Vec3f(); + + // Setup root particles transform + public void tick(OpenMatrix4f objectTransform, Vec3f externalForce, OpenMatrix4f[] poses) { + for (Particle p : this.particleList) { + p.velocity.scale(0.92F); + + p.velocity.add( + externalForce.x * p.rootDistance * p.influence * this.particleMass + , externalForce.y * p.rootDistance * p.influence * this.particleMass + , externalForce.z * p.rootDistance * p.influence * this.particleMass + ); + + if (p.collided) { + VEC3F.set(p.modelPosition); + OpenMatrix4f.transform3v(objectTransform, VEC3F, TRASNFORMED); + p.position.set(TRASNFORMED); + } else { + float influenceInv = 1.0F - p.influence; + + // Apply animation transform + if (influenceInv > 0.0F) { + ClothObject.this.provider.getOriginalMesh().getVertexPosition(p.meshVertexId, POSITION, poses); + VEC3F.set(POSITION.x, POSITION.y, POSITION.z); + OpenMatrix4f.transform3v(objectTransform, VEC3F, TRASNFORMED); + Vec3f.interpolate(p.position, TRASNFORMED, influenceInv, TRASNFORMED); + p.position.set(TRASNFORMED); + } + } + } + } + + private static final Vec3f PARTIAL_VELOCITY = new Vec3f(); + + // Apply external forces, constraints, self collision, and mesh collision + public void substepTick(float substepGravity, float substepDeltaTime, int stepCount, List, ClothSimulator.ClothOBBCollider>> clothColliders) { + for (Particle p : this.particleList) { + p.position.y -= substepGravity * this.particleMass * p.influence; + p.position.add(Vec3f.scale(p.velocity, PARTIAL_VELOCITY, 1.0F / SUB_STEPS)); + } + + for (ConstraintList constraintsBundle : this.constraints) { + float alpha = constraintsBundle.compliance() / (substepDeltaTime * substepDeltaTime); + + for (Constraint c : constraintsBundle.constraints()) { + c.solve(alpha, stepCount); + } + } + + if (stepCount == 1) { + this.buildSpatialHash(); + } + + // Detect self collision + for (Particle p1 : this.particleList) { + int hash = this.getHash(p1.position.x, p1.position.y, p1.position.z); + + for (Particle p2 : this.spatialHash.get(hash)) { + if (p1 == p2) { + continue; + } + + float influenceSum = p1.influence + p2.influence; + + if (influenceSum == 0.0F) { + continue; + } + + Vec3f.sub(p1.position, p2.position, VEC3F); + float length = VEC3F.length(); + + if (length < this.selfCollision) { + float scale = (this.selfCollision - length) / this.selfCollision; + float p1Move = p1.influence / influenceSum; + float p2Move = p2.influence / influenceSum; + VEC3F.scale(scale); + + p1.position.add(VEC3F.x * p1Move, VEC3F.y * p1Move, VEC3F.z * p1Move); + p2.position.sub(VEC3F.x * p2Move, VEC3F.y * p2Move, VEC3F.z * p2Move); + } + } + } + + // Detect collision with mesh collider + if (clothColliders != null) { + for (ConstraintList constraintList : this.constraints) { + if (constraintList.constraintType() == ConstraintType.SHAPING) { + @SuppressWarnings("unchecked") + List constraints = (List)constraintList.constraints(); + List colliders = Lists.newArrayList(); + List destinations = Lists.newArrayList(); + + for (ShapingConstraint constraint : constraints) { + if (constraint.p1.influence == 0.0F && constraint.p2.influence == 0.0F) { + continue; + } + + for (Pair, ClothSimulator.ClothOBBCollider> entry : clothColliders) { + ClothSimulator.ClothOBBCollider clothCollider = entry.getSecond(); + + if (clothCollider.getOuterAABB(this.selfCollision * 0.5F).contains(constraint.p2.position.x, constraint.p2.position.y, constraint.p2.position.z)) { + if (!clothCollider.doesPointCollide(constraint.p1.position.toDoubleVector(), this.selfCollision * 0.5F)) { + colliders.add(entry.getSecond()); + } + } + } + + for (ClothSimulator.ClothOBBCollider collider : colliders) { + collider.pushIfPointInside(constraint.p2.position, constraint.p1.position, this.selfCollision * 0.5F, destinations, colliders); + //collider.pushIfEdgeCollidesCircular(constraint, this.selfCollision * 0.5F, destinations, colliders); + } + /** + int idx = Vec3f.getMostSimilar(constraint.p1.position, constraint.p2.position, destinations); + + if (idx != -1) { + Vec3f mostSimilar = destinations.get(idx); + constraint.p2.position.set(mostSimilar); + } + **/ + int i = Vec3f.getNearest(constraint.p2.position, destinations); + constraint.p2.collided = i != -1; + + if (i != -1) { + Vec3f nearest = destinations.get(i); + Vec3f.sub(nearest, constraint.p2.position, DIFF); + + //constraint.p2.velocity.add(DIFF); + constraint.p2.position.set(nearest); + } + + colliders.clear(); + destinations.clear(); + } + } + } + } + } + + private int getHash(double x, double y, double z) { + int xi = (int)Math.floor(x / SPATIAL_HASH_SPACING); + int yi = (int)Math.floor(y / SPATIAL_HASH_SPACING); + int zi = (int)Math.floor(z / SPATIAL_HASH_SPACING); + int hash = (xi * 92837111) ^ (yi * 689287499) ^ (zi * 283923481); + + return Math.abs(hash) % this.hashTableSize; + } + + public enum ConstraintType { + STRETCHING, SHAPING, BENDING, VOLUME + } + + public static record ConstraintList(float compliance, ConstraintType constraintType, List constraints) { + } + + public static record OffsetParticle(int offsetVertexId, float length, Particle rootParticle, Vec3f position, List positionNormalMembers, List inverseNormal) { + public OffsetParticle copy() { + return new OffsetParticle(this.offsetVertexId, this.length, this.rootParticle, this.position.copy(), this.positionNormalMembers, this.inverseNormal); + } + } + + abstract class Constraint { + abstract void solve(float alpha, int stepcount); + } + + /** + * A constraint that restricts stretching of two particles + */ + class StretchingConstraint extends Constraint { + final Particle p1; + final Particle p2; + final float restLength; + + // Storage vector + static final Vec3f GRADIENT = new Vec3f(); + + StretchingConstraint(Particle p1, Particle p2) { + this.p1 = p1; + this.p2 = p2; + this.restLength = p1.position.distance(p2.position); + } + + @Override + void solve(float alpha, int stepcount) { + float p1Influence = this.p1.influence; + float p2Influence = this.p2.influence; + float influenceSum = p1Influence + p2Influence; + + if (influenceSum < 1E-8) { + return; + } + + Vec3f.sub(this.p2.position, this.p1.position, GRADIENT); + float currentLength = GRADIENT.length(); + + if (currentLength < 1E-8) { + return; + } + + // Normalize + GRADIENT.scale(1.0F / currentLength); + + float constraint = currentLength - this.restLength; + float force = constraint / (influenceSum + alpha); + float p1Move = force * p1Influence; + float p2Move = -force * p2Influence; + + this.p1.position.add(GRADIENT.x * p1Move, GRADIENT.y * p1Move, GRADIENT.z * p1Move); + this.p2.position.add(GRADIENT.x * p2Move, GRADIENT.y * p2Move, GRADIENT.z * p2Move); + } + } + + /** + * A constraint that restricts stretching of two particles, and doesn't allow stretching over the rest length. + * + * Be used to prevent streching too much in gravity direction in low fps + */ + class ShapingConstraint extends Constraint { + final Particle p1; + final Particle p2; + final float restLength; + + // Storage vector + static final Vec3f TOWARD = new Vec3f(); + + ShapingConstraint(Particle p1, Particle p2) { + this.p1 = p1; + this.p2 = p2; + this.restLength = p1.position.distance(p2.position); + } + + @Override + void solve(float alpha, int stepcount) { + float p1Influence = (stepcount == SUB_STEPS && !this.p1.collided) ? 0.0F : this.p1.influence; + float p2Influence = this.p2.influence; + + float influenceSum = p1Influence + p2Influence; + + if (influenceSum < 1E-5) { + return; + } + + Vec3f.sub(this.p2.position, this.p1.position, TOWARD); + float distanceLength = TOWARD.length(); + + if (distanceLength == 0.0F) { + return; + } + + //Normalize + TOWARD.scale(1.0F / distanceLength); + float distanceGap = distanceLength - this.restLength; + float force = distanceGap / (influenceSum + alpha); + float p1Move = force * p1Influence; + float p2Move = -force * p2Influence; + + this.p1.position.add(TOWARD.x * p1Move, TOWARD.y * p1Move, TOWARD.z * p1Move); + this.p2.position.add(TOWARD.x * p2Move, TOWARD.y * p2Move, TOWARD.z * p2Move); + } + } + + /** + * A constraint that restricts bending of member particles. p2, p3 are adjacent edge particles, and p1, p4 are opponent each other + */ + class BendingConstraint extends Constraint { + final Particle p1; + final Particle p2; + final Particle p3; + final Particle p4; + final float restAngle; + final float oppositeDistance; + + // Storage vector + static final Vec3f[] GRADIENTS = { new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f() }; + static final Vec3f NORMAL_SUM = new Vec3f(); + static float STIFFNESS = 1.0F; + + BendingConstraint(Particle p1, Particle p2, Particle p3, Particle p4) { + this.p1 = p1; + this.p2 = p2; + this.p3 = p3; + this.p4 = p4; + this.restAngle = this.getDihedralAngle(); + this.oppositeDistance = Vec3f.sub(this.p1.position, this.p4.position, null).lengthSqr(); + } + + @Override + void solve(float alpha, int stepcount) { + float influenceSum = this.p1.influence + this.p2.influence + this.p3.influence + this.p4.influence; + + if (influenceSum < 1E-8) { + return; + } + + float currentAngle = this.getDihedralAngle(); + float constraint = (this.restAngle - currentAngle); + + while (constraint > Math.PI) { + constraint -= Math.PI * 2; + } + + while (constraint < -Math.PI) { + constraint += Math.PI * 2; + } + + // radian angle * diameter + constraint = this.oppositeDistance * constraint; + + float edgeLength = EDGE.length(); + + CROSS1.scale(edgeLength); + CROSS2.scale(edgeLength); + GRADIENTS[0].set(CROSS1); + GRADIENTS[3].set(CROSS2); + + Vec3f.add(CROSS1, CROSS2, NORMAL_SUM); + NORMAL_SUM.scale(-0.5F); + GRADIENTS[1].set(NORMAL_SUM); + GRADIENTS[2].set(NORMAL_SUM); + + float weight = this.p1.influence * GRADIENTS[0].lengthSqr() + + this.p2.influence * GRADIENTS[1].lengthSqr() + + this.p3.influence * GRADIENTS[2].lengthSqr() + + this.p4.influence * GRADIENTS[3].lengthSqr(); + + if (weight < 1E-8) { + return; + } + + float force = (-constraint * STIFFNESS) / (influenceSum + alpha); + + GRADIENTS[0].scale(force * this.p1.influence); + GRADIENTS[1].scale(force * this.p2.influence); + GRADIENTS[2].scale(force * this.p3.influence); + GRADIENTS[3].scale(force * this.p4.influence); + + Vec3f.add(this.p1.position, GRADIENTS[0], this.p1.position); + Vec3f.add(this.p2.position, GRADIENTS[1], this.p2.position); + Vec3f.add(this.p3.position, GRADIENTS[2], this.p3.position); + Vec3f.add(this.p4.position, GRADIENTS[3], this.p4.position); + } + + static final Vec3f P2P1 = new Vec3f(); + static final Vec3f P3P1 = new Vec3f(); + static final Vec3f P4P2 = new Vec3f(); + static final Vec3f P4P3 = new Vec3f(); + static final Vec3f EDGE = new Vec3f(); + static final Vec3f EDGE_NORM = new Vec3f(); + + static final Vec3f CROSS1 = new Vec3f(); + static final Vec3f CROSS2 = new Vec3f(); + static final Vec3f CROSS3 = new Vec3f(); + + public float getDihedralAngle() { + Vec3f.sub(this.p1.position, this.p2.position, P2P1); + Vec3f.sub(this.p1.position, this.p3.position, P3P1); + Vec3f.sub(this.p4.position, this.p2.position, P4P2); + Vec3f.sub(this.p4.position, this.p3.position, P4P3); + Vec3f.sub(this.p3.position, this.p2.position, EDGE); + + Vec3f.cross(P2P1, P3P1, CROSS1); + Vec3f.cross(P4P3, P4P2, CROSS2); + CROSS1.normalize(); + CROSS2.normalize(); + Vec3f.normalize(EDGE, EDGE_NORM); + + float cos = Vec3f.dot(CROSS1, CROSS2); + float sin = Vec3f.dot(Vec3f.cross(CROSS1, CROSS2, CROSS3), EDGE_NORM); + + return (float)Math.atan2(sin, cos); + } + } + + /** + * A constraint that resists squashing of tetrahedral. + * + * Note: This constraint is expensive. Consider using NormalMappedParticle instead. + */ + class VolumeConstraint extends Constraint { + final Particle[] particles; + final float restVolume; + + static final float SUBDIVISION = 1.0F / 6.0F; + static final int[][] VOLUME_ORDER = { {1, 3, 2}, {0, 2, 3}, {0, 3, 1}, {0, 1, 2} }; + static final Vec3f[] SHRINK_DIRECTIONS = { new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f() }; + + // Storage vectors + static final Vec3f P1_TO_P2 = new Vec3f(); + static final Vec3f P1_TO_P3 = new Vec3f(); + static final Vec3f P1_TO_P4 = new Vec3f(); + static final Vec3f TET_CROSS = new Vec3f(); + + VolumeConstraint(Particle p1, Particle p2, Particle p3, Particle p4) { + this.particles = new Particle[4]; + this.particles[0] = p1; + this.particles[1] = p2; + this.particles[2] = p3; + this.particles[3] = p4; + this.restVolume = this.getTetrahedralVolume(); + } + + @Override + void solve(float alpha, int stepcount) { + float weight = 0.0F; + + for (int i = 0; i < 4; i++) { + Particle p1 = this.particles[VOLUME_ORDER[i][0]]; + Particle p2 = this.particles[VOLUME_ORDER[i][1]]; + Particle p3 = this.particles[VOLUME_ORDER[i][2]]; + + Vec3f.sub(p2.position, p1.position, P1_TO_P2); + Vec3f.sub(p3.position, p1.position, P1_TO_P3); + Vec3f.cross(P1_TO_P2, P1_TO_P3, SHRINK_DIRECTIONS[i]); + SHRINK_DIRECTIONS[i].scale(SUBDIVISION); + + weight += this.particles[i].influence * SHRINK_DIRECTIONS[i].lengthSqr(); + } + + if (weight < 1E-8) { + return; + } + + float constraint = this.restVolume - this.getTetrahedralVolume(); + float force = constraint / (weight + alpha); + + for (int i = 0; i < 4; i++) { + SHRINK_DIRECTIONS[i].scale(force * this.particles[i].influence); + Vec3f.add(this.particles[i].position, SHRINK_DIRECTIONS[i], this.particles[i].position); + } + } + + float getTetrahedralVolume() { + Vec3f.sub(this.particles[1].position, this.particles[0].position, P1_TO_P2); + Vec3f.sub(this.particles[2].position, this.particles[0].position, P1_TO_P3); + Vec3f.sub(this.particles[3].position, this.particles[0].position, P1_TO_P4); + Vec3f.cross(P1_TO_P2, P1_TO_P3, TET_CROSS); + + return Vec3f.dot(TET_CROSS, P1_TO_P4) / 6.0F; + } + } + } + } + + public static class ClothOBBCollider extends OBBCollider { + public ClothOBBCollider(double vertexX, double vertexY, double vertexZ, double centerX, double centerY, double centerZ) { + super(vertexX, vertexY, vertexZ, centerX, centerY, centerZ); + } + + public AABB getOuterAABB(float particleRadius) { + double maxX = -1000000.0D; + double maxY = -1000000.0D; + double maxZ = -1000000.0D; + + for (Vec3 rotated : this.rotatedVertices) { + double xdistance = Math.abs(rotated.x); + + if (xdistance > maxX) { + maxX = xdistance; + } + + double ydistance = Math.abs(rotated.y); + + if (ydistance > maxY) { + maxY = ydistance; + } + + double zdistance = Math.abs(rotated.z); + + if (zdistance > maxZ) { + maxZ = zdistance; + } + } + + maxX += particleRadius; + maxY += particleRadius; + maxZ += particleRadius; + + return new AABB(-maxX, -maxY, -maxZ, maxX, maxY, maxZ).move(this.worldCenter); + } + + private boolean doesPointCollide(Vec3 point, float radius) { + Vec3 toOpponent = point.subtract(this.worldCenter); + + for (Vec3 seperateAxis : this.rotatedNormals) { + Vec3 maxProj = null; + double maxDot = -1000000.0D; + + if (seperateAxis.dot(toOpponent) < 0.0D) { + seperateAxis = seperateAxis.scale(-1.0D); + } + + for (Vec3 vertexVector : this.rotatedVertices) { + Vec3 toVertex = seperateAxis.dot(vertexVector) > 0.0D ? vertexVector : vertexVector.scale(-1.0D); + double dot = seperateAxis.dot(toVertex); + + if (dot > maxDot || maxProj == null) { + maxDot = dot; + maxProj = toVertex; + } + } + + Vec3 opponentProjection = MathUtils.projectVector(toOpponent, seperateAxis); + Vec3 vertexProjection = MathUtils.projectVector(maxProj, seperateAxis); + + if (opponentProjection.length() > vertexProjection.length() + radius) { + return false; + } + } + + return true; + } + /** + public void pushIfEdgePenetrates(ClothSimulator.ClothObject.ClothPart.ShapingConstraint constraint, float particleRadius) { + Vec3f.sub(constraint.p1.position, constraint.p2.position, UNIT); + float restLength = UNIT.length(); + + if (restLength == 0.0F) { + return; + } + + for (int i = 0; i < RECTANGLES.length; i++) { + this.getPos(RECTANGLES[i][0], V1); + this.getPos(RECTANGLES[i][1], V2); + this.getPos(RECTANGLES[i][2], V3); + this.getPos(RECTANGLES[i][3], V4); + + NORMAL.set(this.rotatedNormals[i / 2]); + + if (i % 2 == 1) { + NORMAL.scale(-1.0F); + } + + getLinePlaneIntersectPoint(constraint.p1.position, UNIT, restLength, NORMAL, INTERSECT); + + if (INTERSECT.validateValues()) { + constraint.p2.position.set(INTERSECT); + } + } + } + **/ + private static final Vec3f WORLD_CENTER = new Vec3f(); + private static final Vec3f TO_OPPONENT = new Vec3f(); + private static final Vec3f SEP_AXIS = new Vec3f(); + private static final Vec3f TO_VERTEX = new Vec3f(); + private static final Vec3f MAX_PROJ = new Vec3f(); + private static final Vec3f TO_OPPONENT_PROJECTION = new Vec3f(); + private static final Vec3f VERTEX_PROJECTION = new Vec3f(); + private static final Vec3f PROJECTION1 = new Vec3f(); + private static final Vec3f PROJECTION2 = new Vec3f(); + private static final Vec3f PROJECTION3 = new Vec3f(); + private static final Vec3f TO_PLANE1 = new Vec3f(); + private static final Vec3f TO_PLANE2 = new Vec3f(); + private static final Vec3f TO_PLANE3 = new Vec3f(); + + private final Vec3f[] destinations = { new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f() }; + + /** + * Push back the second particle of shping constraint from OBB + * + * @param constraint + * @param selfCollision + * @param destnations + * @param others + */ + public void pushIfPointInside(Vec3f point, Vec3f root, float selfCollision, List destnations, List others) { + WORLD_CENTER.set(this.worldCenter); + Vec3f.sub(point, WORLD_CENTER, TO_OPPONENT); + + int order = 0; + + for (Vec3 seperateAxis : this.rotatedNormals) { + SEP_AXIS.set(seperateAxis); + float maxDot = -10000.0F; + + if (Vec3f.dot(SEP_AXIS, TO_OPPONENT) < 0.0D) { + SEP_AXIS.scale(-1.0F); + } + + for (Vec3 vertexVector : this.rotatedVertices) { + TO_VERTEX.set(vertexVector); + + if (Vec3f.dot(SEP_AXIS, TO_VERTEX) < 0.0D) { + TO_VERTEX.scale(-1.0F); + } + + float dot = Vec3f.dot(SEP_AXIS, TO_VERTEX); + + if (dot > maxDot) { + maxDot = dot; + MAX_PROJ.set(TO_VERTEX); + } + } + + MathUtils.projectVector(TO_OPPONENT, SEP_AXIS, TO_OPPONENT_PROJECTION); + MathUtils.projectVector(MAX_PROJ, SEP_AXIS, VERTEX_PROJECTION); + + if (TO_OPPONENT_PROJECTION.length() > VERTEX_PROJECTION.length() + selfCollision) { + return; + } else { + switch (order) { + case 0 -> { + PROJECTION1.set(TO_OPPONENT_PROJECTION); + Vec3f.scale(VERTEX_PROJECTION, TO_PLANE1, (VERTEX_PROJECTION.length() + selfCollision) / VERTEX_PROJECTION.length()); + } + case 1 -> { + PROJECTION2.set(TO_OPPONENT_PROJECTION); + Vec3f.scale(VERTEX_PROJECTION, TO_PLANE2, (VERTEX_PROJECTION.length() + selfCollision) / VERTEX_PROJECTION.length()); + } + case 2 -> { + PROJECTION3.set(TO_OPPONENT_PROJECTION); + Vec3f.scale(VERTEX_PROJECTION, TO_PLANE3, (VERTEX_PROJECTION.length() + selfCollision) / VERTEX_PROJECTION.length()); + } + } + } + + order++; + } + + this.destinations[0].set(0.0F, 0.0F, 0.0F).add(PROJECTION1).add(PROJECTION2).add(TO_PLANE3).add(this.worldCenter); + this.destinations[1].set(0.0F, 0.0F, 0.0F).add(PROJECTION2).add(PROJECTION3).add(TO_PLANE1).add(this.worldCenter); + this.destinations[2].set(0.0F, 0.0F, 0.0F).add(PROJECTION3).add(PROJECTION1).add(TO_PLANE2).add(this.worldCenter); + this.destinations[3].set(0.0F, 0.0F, 0.0F).add(PROJECTION1).add(PROJECTION2).sub(TO_PLANE3).add(this.worldCenter); + this.destinations[4].set(0.0F, 0.0F, 0.0F).add(PROJECTION2).add(PROJECTION3).sub(TO_PLANE1).add(this.worldCenter); + this.destinations[5].set(0.0F, 0.0F, 0.0F).add(PROJECTION3).add(PROJECTION1).sub(TO_PLANE2).add(this.worldCenter); + + Loop1: + for (Vec3f dest : this.destinations) { + for (ClothOBBCollider other : others) { + if (other == this) { + continue; + } + + if (other.doesPointCollide(dest.toDoubleVector(), selfCollision * 0.5F)) { + dest.invalidate(); + continue Loop1; + } + } + } + + for (Vec3f dest : this.destinations) { + if (dest.validateValues()) { + destnations.add(dest); + } + } + } + + /** + * Util functions + */ + /** + private static final Vec3f EDGE = new Vec3f(); + private static final Vec3f TO_POINT = new Vec3f(); + private static final Vec3f PREV_CROSS = new Vec3f(); + private static final Vec3f CROSS = new Vec3f(); + + private static boolean isPointInRectangle(Vec3f point, Vec3f... points) { + PREV_CROSS.invalidate(); + + for (int i = 0; i < 4; i++) { + Vec3f.sub(points[(i + 1) % 4], points[i], EDGE); + Vec3f.sub(point, points[(i + 1) % 4], TO_POINT); + Vec3f.cross(EDGE, TO_POINT, CROSS); + + if (!PREV_CROSS.validateValues()) { + PREV_CROSS.set(CROSS); + continue; + } else { + if (Vec3f.dot(PREV_CROSS, CROSS) <= 0.0F) { + return false; + } + PREV_CROSS.set(CROSS); + } + } + + return true; + } + + private static final int[][] RECTANGLES = { {0, 1, 7, 6}, {2, 3, 5, 4}, {0, 3, 2, 1}, {4, 5, 6, 7}, {1, 2, 4, 7}, {0, 6, 5, 3} }; + + public boolean intersectLine(Vec3f p1, Vec3f p2) { + Vec3f.sub(p1, p2, UNIT); + float restLength = UNIT.length(); + + if (restLength == 0.0F) { + return false; + } + + for (int i = 0; i < RECTANGLES.length; i++) { + this.getPos(RECTANGLES[i][0], V1); + this.getPos(RECTANGLES[i][1], V2); + this.getPos(RECTANGLES[i][2], V3); + this.getPos(RECTANGLES[i][3], V4); + + NORMAL.set(this.rotatedNormals[i / 2]); + + if (i % 2 == 1) { + NORMAL.scale(-1.0F); + } + + if (intersectLinePlane(p1, UNIT, restLength, NORMAL)) { + return true; + } + } + + return false; + } + + private static final Vec3f LINE_START_TO_PLANE_POINT = new Vec3f(); + + private static boolean intersectLinePlane(Vec3f lineStart, Vec3f unitVector, float lineLength, Vec3f planeNormal) { + float dot1 = Vec3f.dot(unitVector, planeNormal); + + // Inner -> outer intersection (dot1 > 0.0F) + // Objects are parallel (dot1 == 0.0F) + if (dot1 >= 0.0F) { + return false; + } + + Vec3f.sub(V1, lineStart, LINE_START_TO_PLANE_POINT); + float dot3 = Vec3f.dot(LINE_START_TO_PLANE_POINT, planeNormal); + + // Objects are parallel + if (dot3 == 0.0F) { + return false; + } + + float dot2 = dot3 / dot1; + + // intersecting point is outside of line + if (dot2 < 0.0F || Math.abs(dot2) > lineLength) { + return false; + } + + unitVector.scale(dot2); + Vec3f.add(lineStart, unitVector, INTERSECT); + unitVector.scale(1.0F / dot2); + + if (!isPointInRectangle(INTERSECT, V1, V2, V3, V4)) { + return false; + } + + return true; + } + + private static void getLinePlaneIntersectPoint(Vec3f lineStart, Vec3f unitVector, float lineLength, Vec3f planeNormal, Vec3f result) { + float dot1 = Vec3f.dot(unitVector, planeNormal); + + // Inner -> outer intersection (dot1 > 0.0F) + // Objects are parallel (dot1 == 0.0F) + if (dot1 >= 0.0F) { + result.invalidate(); + return; + } + + Vec3f.sub(V1, lineStart, LINE_START_TO_PLANE_POINT); + float dot3 = Vec3f.dot(LINE_START_TO_PLANE_POINT, planeNormal); + + // Objects are parallel + if (dot3 == 0.0F) { + result.invalidate(); + return; + } + + float dot2 = dot3 / dot1; + + // intersecting point is outside of line + if (dot2 < 0.0F || Math.abs(dot2) > lineLength) { + result.invalidate(); + return; + } + + unitVector.scale(dot2); + Vec3f.add(lineStart, unitVector, result); + unitVector.scale(1.0F / dot2); + + if (!isPointInRectangle(result, V1, V2, V3, V4)) { + result.invalidate(); + } + } + + private void getPos(int idx, Vec3f v) { + if (idx >= this.rotatedVertices.length) { + idx -= this.rotatedVertices.length; + v.x = (float)(this.worldCenter.x - this.rotatedVertices[idx].x); + v.y = (float)(this.worldCenter.y - this.rotatedVertices[idx].y); + v.z = (float)(this.worldCenter.z - this.rotatedVertices[idx].z); + } else { + v.x = (float)(this.worldCenter.x + this.rotatedVertices[idx].x); + v.y = (float)(this.worldCenter.y + this.rotatedVertices[idx].y); + v.z = (float)(this.worldCenter.z + this.rotatedVertices[idx].z); + } + } + + private static final Vec3f UNIT_VEC = new Vec3f(); + private static final Vec3f INTERSECTING = new Vec3f(); + private static final Vec3f CIRCLE_CENTER_TO_INTERSECTING = new Vec3f(); + + private static void circlepointTouchingEdge(Vec3f circleCenter, Vec3f circleNormal, float circleRadius, Vec3f edgeStart, Vec3f edgeEnd, Vec3f intersect, Vec3 worldCenter) { + Vec3f.sub(edgeEnd, edgeStart, UNIT_VEC); + float edgeLength = UNIT_VEC.length(); + UNIT_VEC.scale(1.0F / edgeLength); + + float dot1 = Vec3f.dot(UNIT_VEC, circleNormal); + + // Objects are parallel (dot1 == 0.0F) + if (dot1 == 0.0F) { + intersect.invalidate(); + return; + } + + Vec3f.sub(circleCenter, edgeStart, LINE_START_TO_PLANE_POINT); + float dot3 = Vec3f.dot(LINE_START_TO_PLANE_POINT, circleNormal); + + // Objects are parallel + if (dot3 == 0.0F) { + intersect.invalidate(); + return; + } + + float dot2 = dot3 / dot1; + + // intersecting point is outside of line + if (dot2 < 0.0F || Math.abs(dot2) > edgeLength) { + intersect.invalidate(); + return; + } + + UNIT_VEC.scale(dot2); + Vec3f.add(edgeStart, UNIT_VEC, INTERSECTING); + Vec3f.sub(INTERSECTING, circleCenter, CIRCLE_CENTER_TO_INTERSECTING); + float len = CIRCLE_CENTER_TO_INTERSECTING.length(); + + // Intersecting point is outside of circle + if (len > circleRadius) { + intersect.invalidate(); + } else { + // Expand toward intersecting point edge as the circle radius + CIRCLE_CENTER_TO_INTERSECTING.scale(circleRadius / len); + Vec3f.add(circleCenter, CIRCLE_CENTER_TO_INTERSECTING, intersect); + } + } + + private static final Vec3f PLANE_TO_CIRCLE_CENTER = new Vec3f(); + private static final Vec3f NORM_SCALED = new Vec3f(); + private static final Vec3f PROJ_CENTER = new Vec3f(); + private static final Vec3f PERPENDICULAR_VECTOR = new Vec3f(); + private static final Vec3f INTERSECT_POINT = new Vec3f(); + + private void intersectCirclePlane(Vec3f circleCenter, Vec3f circleNormal, float circleRadius, Vec3f planeNormal, Vec3f intersect1, Vec3f intersect2) { + // Compute signed distance from circle center to plane + float d = Vec3f.dot(Vec3f.sub(circleCenter, V1, PLANE_TO_CIRCLE_CENTER), planeNormal); + + if (Math.abs(d) > circleRadius) { // No intersection + intersect1.invalidate(); + intersect2.invalidate(); + } else if (Math.abs(d) == circleRadius) { // One intersection point + Vec3f.sub(circleCenter, Vec3f.scale(planeNormal, NORM_SCALED, d), intersect1); + + if (!isPointInRectangle(intersect1, V1, V2, V3, V4)) { + intersect1.invalidate(); + } + + intersect2.invalidate(); + } else { // Two intersection points + Vec3f.sub(circleCenter, Vec3f.scale(planeNormal, NORM_SCALED, d), PROJ_CENTER); + float chordRadius = (float)Math.sqrt(circleRadius * circleRadius - d * d); + Vec3f.cross(circleNormal, planeNormal, PERPENDICULAR_VECTOR); + + float normLength = PERPENDICULAR_VECTOR.length(); + + if (normLength == 0.0F) { + return; + } + + PERPENDICULAR_VECTOR.scale(chordRadius / normLength); + + // Compute a vector toward intersecting points + + Vec3f.add(circleCenter, PERPENDICULAR_VECTOR, intersect1); + Vec3f.add(circleCenter, PERPENDICULAR_VECTOR.scale(-1.0F), intersect2); + + if (!isPointInRectangle(Vec3f.add(circleCenter, intersect1, INTERSECT_POINT), V1, V2, V3, V4)) { + intersect1.invalidate(); + } + + if (!isPointInRectangle(Vec3f.add(circleCenter, intersect2, INTERSECT_POINT), V1, V2, V3, V4)) { + intersect2.invalidate(); + } + + if (intersect1.validateValues() && intersect2.validateValues()) { + double dist1 = this.worldCenter.distanceToSqr(intersect1.x, intersect1.y, intersect1.z); + double dist2 = this.worldCenter.distanceToSqr(intersect2.x, intersect2.y, intersect2.z); + + if (dist1 > dist2) { + intersect2.invalidate(); + } else { + intersect1.invalidate(); + } + } + } + } + + private static void intersectLinePlane(Vec3f lineStart, Vec3f unitVector, float lineLength, Vec3f planeNormal, Vec3f intersect) { + float dot1 = Vec3f.dot(unitVector, planeNormal); + + // Inner -> outer intersection (dot1 > 0.0F) + // Objects are parallel (dot1 == 0.0F) + if (dot1 >= 0.0F) { + intersect.invalidate(); + return; + } + + Vec3f.sub(V1, lineStart, LINE_START_TO_PLANE_POINT); + float dot3 = Vec3f.dot(LINE_START_TO_PLANE_POINT, planeNormal); + + // Objects are parallel + if (dot3 == 0.0F) { + intersect.invalidate(); + return; + } + + float dot2 = dot3 / dot1; + + // intersecting point is outside of line + if (dot2 < 0.0F || Math.abs(dot2) > lineLength) { + intersect.invalidate(); + return; + } + + unitVector.scale(dot2); + Vec3f.add(lineStart, unitVector, intersect); + unitVector.scale(1.0F / dot2); + + if (!isPointInRectangle(intersect, V1, V2, V3, V4)) { + intersect.invalidate(); + } + } + + private static final Quaternionf ROTATOR = new Quaternionf(); + private static final Vec3f PITCH_AXIS = new Vec3f(); + private static final Vec3f YAW_AXIS = new Vec3f(); + private static final Vec3f NORMALIZED_TO_INTERSECT = new Vec3f(); + + private static final int[][] EDGES = { {0, 1}, {1, 2}, {2, 3}, {3, 0}, {0, 6}, {1, 7}, {2, 4}, {3, 5}, {4, 5}, {5, 6}, {6, 7}, {7, 4} }; + private static final List> EDGE_ADJACENT_PLANES = List.of( + Set.of(0, 2) + , Set.of(2, 4) + , Set.of(1, 2) + , Set.of(2, 5) + , Set.of(0, 5) + , Set.of(0, 4) + , Set.of(1, 4) + , Set.of(1, 5) + , Set.of(1, 3) + , Set.of(3, 5) + , Set.of(0, 3) + , Set.of(3, 4) + ); + + private static final Vec3f V1 = new Vec3f(); + private static final Vec3f V2 = new Vec3f(); + private static final Vec3f V3 = new Vec3f(); + private static final Vec3f V4 = new Vec3f(); + private static final Vec3f NORMAL = new Vec3f(); + private static final Vec3f UNIT = new Vec3f(); + private static final Vec3f INTERSECT = new Vec3f(); + + private final Vec3f[] intersects = { + // Circle plane intersections + new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f(), new Vec3f(), new Vec3f() + // Circle edge intersections + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + , new Vec3f(), new Vec3f() + }; + + public void pushIfEdgeCollidesCircular(ClothSimulator.ClothObject.ClothPart.ShapingConstraint constraint, float selfCollision, List destinations, List others) { + Vec3f.sub(constraint.p2.position, constraint.p1.position, UNIT); + float restLength = constraint.restLength;//UNIT.length(); + + if (restLength == 0.0F) { + return; + } + + // Get rotator of p1 -> p2 + Vec3f.getRotatorBetween(Vec3f.Z_AXIS, UNIT, ROTATOR); + + // Setup two normal vectors of circles + PITCH_AXIS.set(1.0F, 0.0F, 0.0F); + YAW_AXIS.set(0.0F, 1.0F, 0.0F); + Vec3f.rotate(ROTATOR, PITCH_AXIS, PITCH_AXIS); + Vec3f.rotate(ROTATOR, YAW_AXIS, YAW_AXIS); + + UNIT.normalize(); + //UNIT.scale(1.0F / restLength); + + int intersectRects = 0; + + for (int i = 0; i < RECTANGLES.length; i++) { + this.getPos(RECTANGLES[i][0], V1); + this.getPos(RECTANGLES[i][1], V2); + this.getPos(RECTANGLES[i][2], V3); + this.getPos(RECTANGLES[i][3], V4); + + NORMAL.set(this.rotatedNormals[i / 2]); + + if (i % 2 == 1) { + NORMAL.scale(-1.0F); + } + + intersectLinePlane(constraint.p1.position, UNIT, restLength, NORMAL, INTERSECT); + + if (INTERSECT.validateValues()) { + // Check plane - circle intersections + this.intersectCirclePlane(constraint.p1.position, PITCH_AXIS, restLength, NORMAL, this.intersects[i * 4], this.intersects[i * 4 + 1]); + this.intersectCirclePlane(constraint.p1.position, YAW_AXIS, restLength, NORMAL, this.intersects[i * 4 + 2], this.intersects[i * 4 + 3]); + intersectRects++; + } else { + this.intersects[i * 4].invalidate(); + this.intersects[i * 4 + 1].invalidate(); + this.intersects[i * 4 + 2].invalidate(); + this.intersects[i * 4 + 3].invalidate(); + } + } + + // If no planes intersect with edge, do nothing. + if (intersectRects == 0) { + return; + } + + for (int i = 0; i < EDGES.length; i++) { + this.getPos(EDGES[i][0], V1); + this.getPos(EDGES[i][1], V2); + + // Check edge - circle intersections + circlepointTouchingEdge(constraint.p1.position, PITCH_AXIS, restLength, V1, V2, this.intersects[24 + i * 2], this.worldCenter); + circlepointTouchingEdge(constraint.p1.position, YAW_AXIS, restLength, V1, V2, this.intersects[24 + i * 2 + 1], this.worldCenter); + } + + // Exclude points that penetrates obb rectangle if root particle is outside of obb + if (this.doesPointCollide(constraint.p1.position.toDoubleVector(), 0.0F)) { + for (int i = 0; i < this.intersects.length; i++) { + if (!this.intersects[i].validateValues()) { + continue; + } else { + for (int j = 0; j < RECTANGLES.length; j++) { + if (i > 23) { + // Skip the planes that adjacent to collider edge + if (EDGE_ADJACENT_PLANES.get((i - 24) / 2).contains(j)) { + continue; + } + } else { + // Skip the planes if this intersecting point created by itself + if (i / 4 == j) { + continue; + } + } + + this.getPos(RECTANGLES[j][0], V1); + this.getPos(RECTANGLES[j][1], V2); + this.getPos(RECTANGLES[j][2], V3); + this.getPos(RECTANGLES[j][3], V4); + + NORMAL.set(this.rotatedNormals[j / 2]); + + if (j % 2 == 1) { + NORMAL.scale(-1.0F); + } + + intersectLinePlane(constraint.p1.position, Vec3f.normalize(this.intersects[i], NORMALIZED_TO_INTERSECT), restLength, NORMAL, INTERSECT); + + if (INTERSECT.validateValues()) { + this.intersects[i].invalidate(); + break; + } + } + } + } + } + + // Exclude points that intersects with other OBBs + Loop1: + for (Vec3f dest : this.destinations) { + for (ClothOBBCollider other : others) { + if (other == this) { + continue; + } + + if (other.doesPointCollide(dest.toDoubleVector(), selfCollision)) { + dest.invalidate(); + continue Loop1; + } + } + } + + for (Vec3f dest : this.intersects) { + if (dest.validateValues()) { + destinations.add(dest); + } + } + }**/ + } +} diff --git a/src/main/java/com/tiedup/remake/rig/event/PatchedRenderersEvent.java b/src/main/java/com/tiedup/remake/rig/event/PatchedRenderersEvent.java new file mode 100644 index 0000000..e0dd8b1 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/event/PatchedRenderersEvent.java @@ -0,0 +1,69 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.event; + +import java.util.Map; +import java.util.function.Function; + +import com.google.gson.JsonElement; + +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.fml.event.IModBusEvent; +import com.tiedup.remake.rig.render.PatchedEntityRenderer; +import com.tiedup.remake.rig.render.item.RenderItemBase; + +@SuppressWarnings("rawtypes") +public abstract class PatchedRenderersEvent extends Event implements IModBusEvent { + public static class RegisterItemRenderer extends PatchedRenderersEvent { + private final Map> itemRenderers; + + public RegisterItemRenderer(Map> itemRenderers) { + this.itemRenderers = itemRenderers; + } + + public void addItemRenderer(ResourceLocation rl, Function provider) { + if (this.itemRenderers.containsKey(rl)) { + throw new IllegalArgumentException("Item renderer " + rl + " already registered."); + } + + this.itemRenderers.put(rl, provider); + } + } + + public static class Add extends PatchedRenderersEvent { + private final Map, Function, PatchedEntityRenderer>> entityRendererProvider; + private final EntityRendererProvider.Context context; + + public Add(Map, Function, PatchedEntityRenderer>> entityRendererProvider, EntityRendererProvider.Context context) { + this.entityRendererProvider = entityRendererProvider; + this.context = context; + } + + public void addPatchedEntityRenderer(EntityType entityType, Function, PatchedEntityRenderer> provider) { + this.entityRendererProvider.put(entityType, provider); + } + + public EntityRendererProvider.Context getContext() { + return this.context; + } + } + + public static class Modify extends PatchedRenderersEvent { + private final Map, PatchedEntityRenderer> renderers; + + public Modify(Map, PatchedEntityRenderer> renderers) { + this.renderers = renderers; + } + + public PatchedEntityRenderer get(EntityType entityType) { + return this.renderers.get(entityType); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/event/PrepareModelEvent.java b/src/main/java/com/tiedup/remake/rig/event/PrepareModelEvent.java new file mode 100644 index 0000000..ab41d0a --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/event/PrepareModelEvent.java @@ -0,0 +1,64 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.event; + +import com.mojang.blaze3d.vertex.PoseStack; + +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraftforge.eventbus.api.Event; +import com.tiedup.remake.rig.mesh.SkinnedMesh; +import com.tiedup.remake.rig.render.PatchedEntityRenderer; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class PrepareModelEvent extends Event { + private final SkinnedMesh mesh; + private final LivingEntityPatch entitypatch; + private final MultiBufferSource buffer; + private final PoseStack poseStack; + private final int packedLight; + private final float partialTicks; + + private final PatchedEntityRenderer renderer; + + public PrepareModelEvent(PatchedEntityRenderer renderer, SkinnedMesh mesh, LivingEntityPatch entitypatch, MultiBufferSource buffer, PoseStack poseStack, int packedLight, float partialTicks) { + this.renderer = renderer; + this.mesh = mesh; + this.entitypatch = entitypatch; + this.buffer = buffer; + this.poseStack = poseStack; + this.packedLight = packedLight; + this.partialTicks = partialTicks; + } + + public SkinnedMesh getMesh() { + return this.mesh; + } + + public LivingEntityPatch getEntityPatch() { + return this.entitypatch; + } + + public MultiBufferSource getBuffer() { + return this.buffer; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public int getPackedLight() { + return this.packedLight; + } + + public float getPartialTicks() { + return this.partialTicks; + } + + public PatchedEntityRenderer getRenderer() { + return this.renderer; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/event/RegisterResourceLayersEvent.java b/src/main/java/com/tiedup/remake/rig/event/RegisterResourceLayersEvent.java new file mode 100644 index 0000000..96d6dd9 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/event/RegisterResourceLayersEvent.java @@ -0,0 +1,30 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.event; + +import java.util.Map; + +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.LivingEntity; +import net.minecraftforge.eventbus.api.Event; +import com.tiedup.remake.rig.mesh.SkinnedMesh; +import com.tiedup.remake.rig.render.layer.LayerUtil; +import com.tiedup.remake.rig.patch.LivingEntityPatch; + +public class RegisterResourceLayersEvent, M extends EntityModel, R extends LivingEntityRenderer, AM extends SkinnedMesh> extends Event { + private final Map> layersbyid; + + public RegisterResourceLayersEvent(Map> layersbyid) { + this.layersbyid = layersbyid; + } + + public void register(ResourceLocation rl, LayerUtil.LayerProvider layerAdder) { + this.layersbyid.put(rl, layerAdder); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/exception/AnimationInvokeException.java b/src/main/java/com/tiedup/remake/rig/exception/AnimationInvokeException.java new file mode 100644 index 0000000..40a4ada --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/exception/AnimationInvokeException.java @@ -0,0 +1,26 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.exception; + +public class AnimationInvokeException extends RuntimeException { + /** + * + */ + private static final long serialVersionUID = 1L; + + public AnimationInvokeException(String message) { + super(message); + } + + public AnimationInvokeException(String message, Throwable cause) { + super(message, cause); + } + + public AnimationInvokeException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/tiedup/remake/rig/exception/AssetLoadingException.java b/src/main/java/com/tiedup/remake/rig/exception/AssetLoadingException.java new file mode 100644 index 0000000..10dfba9 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/exception/AssetLoadingException.java @@ -0,0 +1,22 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.exception; + +public class AssetLoadingException extends RuntimeException { + /** + * + */ + private static final long serialVersionUID = 1L; + + public AssetLoadingException(String message) { + super(message); + } + + public AssetLoadingException(String message, Throwable ex) { + super(message, ex); + } +} diff --git a/src/main/java/com/tiedup/remake/rig/exception/DatapackException.java b/src/main/java/com/tiedup/remake/rig/exception/DatapackException.java new file mode 100644 index 0000000..d69a2c4 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/exception/DatapackException.java @@ -0,0 +1,15 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.exception; + +public class DatapackException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public DatapackException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tiedup/remake/rig/exception/ShaderParsingException.java b/src/main/java/com/tiedup/remake/rig/exception/ShaderParsingException.java new file mode 100644 index 0000000..4aaeb11 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/exception/ShaderParsingException.java @@ -0,0 +1,26 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.exception; + +public class ShaderParsingException extends RuntimeException { + /** + * + */ + private static final long serialVersionUID = 1L; + + public ShaderParsingException(String message) { + super(message); + } + + public ShaderParsingException(String message, Throwable cause) { + super(message, cause); + } + + public ShaderParsingException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/tiedup/remake/rig/math/AnimationTransformEntry.java b/src/main/java/com/tiedup/remake/rig/math/AnimationTransformEntry.java new file mode 100644 index 0000000..22dd11f --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/AnimationTransformEntry.java @@ -0,0 +1,46 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +import java.util.Map; + +import com.google.common.collect.Maps; +import com.mojang.datafixers.util.Pair; + +import com.tiedup.remake.rig.armature.JointTransform; + +public class AnimationTransformEntry { + private static final String[] BINDING_PRIORITY = {JointTransform.PARENT, JointTransform.JOINT_LOCAL_TRANSFORM, JointTransform.ANIMATION_TRANSFORM, JointTransform.RESULT1, JointTransform.RESULT2}; + private final Map> matrices = Maps.newHashMap(); + + public void put(String entryPosition, OpenMatrix4f matrix) { + this.put(entryPosition, matrix, OpenMatrix4f::mul); + } + + public void put(String entryPosition, OpenMatrix4f matrix, MatrixOperation operation) { + if (this.matrices.containsKey(entryPosition)) { + Pair appliedTransform = this.matrices.get(entryPosition); + OpenMatrix4f result = appliedTransform.getSecond().mul(appliedTransform.getFirst(), matrix, null); + this.matrices.put(entryPosition, Pair.of(result, operation)); + } else { + this.matrices.put(entryPosition, Pair.of(new OpenMatrix4f(matrix), operation)); + } + } + + public OpenMatrix4f getResult() { + OpenMatrix4f result = new OpenMatrix4f(); + + for (String entryName : BINDING_PRIORITY) { + if (this.matrices.containsKey(entryName)) { + Pair pair = this.matrices.get(entryName); + pair.getSecond().mul(result, pair.getFirst(), result); + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/math/MathUtils.java b/src/main/java/com/tiedup/remake/rig/math/MathUtils.java new file mode 100644 index 0000000..e4e4657 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/MathUtils.java @@ -0,0 +1,599 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.joml.Math; +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.joml.Vector4f; +import org.joml.Vector4i; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; + +import net.minecraft.client.Camera; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec2; +import net.minecraft.world.phys.Vec3; + +public class MathUtils { + public static final Vec3 XP = new Vec3(1.0D, 0.0D, 0.0D); + public static final Vec3 XN = new Vec3(-1.0D, 0.0D, 0.0D); + public static final Vec3 YP = new Vec3(0.0D, 1.0D, 0.0D); + public static final Vec3 YN = new Vec3(0.0D, -1.0D, 0.0D); + public static final Vec3 ZP = new Vec3(0.0D, 0.0D, 1.0D); + public static final Vec3 ZN = new Vec3(0.0D, 0.0D, -1.0D); + + public static OpenMatrix4f getModelMatrixIntegral(float xPosO, float xPos, float yPosO, float yPos, float zPosO, float zPos, float xRotO, float xRot, float yRotO, float yRot, float partialTick, float scaleX, float scaleY, float scaleZ) { + OpenMatrix4f modelMatrix = new OpenMatrix4f(); + Vec3f translation = new Vec3f(-(xPosO + (xPos - xPosO) * partialTick), ((yPosO + (yPos - yPosO) * partialTick)), -(zPosO + (zPos - zPosO) * partialTick)); + float partialXRot = Mth.rotLerp(partialTick, xRotO, xRot); + float partialYRot = Mth.rotLerp(partialTick, yRotO, yRot); + modelMatrix.translate(translation).rotateDeg(-partialYRot, Vec3f.Y_AXIS).rotateDeg(-partialXRot, Vec3f.X_AXIS).scale(scaleX, scaleY, scaleZ); + + return modelMatrix; + } + + /** + * Blender 2.79 bezier curve + * @param t: 0 ~ 1 + * @retur + */ + public static double bezierCurve(double t) { + double p1 = 0.0D; + double p2 = 0.0D; + double p3 = 1.0D; + double p4 = 1.0D; + double v1, v2, v3, v4; + + v1 = p1; + v2 = 3.0D * (p2 - p1); + v3 = 3.0D * (p1 - 2.0D * p2 + p3); + v4 = p4 - p1 + 3.0D * (p2 - p3); + + return v1 + t * v2 + t * t * v3 + t * t * t * v4; + } + + public static float bezierCurve(float t) { + return (float)bezierCurve((double)t); + } + + public static int getSign(double value) { + return value > 0.0D ? 1 : -1; + } + + public static Vec3 getVectorForRotation(float pitch, float yaw) { + float f = pitch * (float) Math.PI / 180F; + float f1 = -yaw * (float) Math.PI / 180F; + float f2 = Mth.cos(f1); + float f3 = Mth.sin(f1); + float f4 = Mth.cos(f); + float f5 = Mth.sin(f); + + return new Vec3(f3 * f4, -f5, f2 * f4); + } + + public static float lerpBetween(float f1, float f2, float zero2one) { + float f = 0; + + for (f = f2 - f1; f < -180.0F; f += 360.0F) { + } + + while (f >= 180.0F) { + f -= 360.0F; + } + + return f1 + zero2one * f; + } + + public static float rotlerp(float from, float to, float limit) { + float f = Mth.wrapDegrees(to - from); + + if (f > limit) { + f = limit; + } + + if (f < -limit) { + f = -limit; + } + + float f1 = from + f; + + while (f1 >= 180.0F) { + f1 -= 360.0F; + } + + while (f1 <= -180.0F) { + f1 += 360.0F; + } + + return f1; + } + + public static float rotWrap(double d) { + while (d >= 180.0) { + d -= 360.0; + } + while (d < -180.0) { + d += 360.0; + } + return (float)d; + } + + public static float wrapRadian(float pValue) { + float maxRot = (float)Math.PI * 2.0F; + float f = pValue % maxRot; + + if (f >= Math.PI) { + f -= maxRot; + } + + if (f < -Math.PI) { + f += maxRot; + } + + return f; + } + + public static float lerpDegree(float from, float to, float progression) { + from = Mth.wrapDegrees(from); + to = Mth.wrapDegrees(to); + + if (Math.abs(from - to) > 180.0F) { + if (to < 0.0F) { + from -= 360.0F; + } else if (to > 0.0F) { + from += 360.0F; + } + } + + return Mth.lerp(progression, from, to); + } + + public static float findNearestRotation(float src, float rotation) { + float diff = Math.abs(src - rotation); + float idealRotation = rotation; + int sign = Mth.sign(src - rotation); + + if (sign == 0) { + return rotation; + } + + while (true) { + float next = idealRotation + sign * 360.0F; + + if (Math.abs(src - next) > diff) { + return idealRotation; + } + + idealRotation = next; + diff = Math.abs(src - next); + } + } + + public static Vec3 getNearestVector(Vec3 from, Vec3... vectors) { + double minLength = 1000000.0D; + int index = 0; + + for (int i = 0; i < vectors.length; i++) { + if (vectors[i] == null) { + continue; + } + + double distSqr = from.distanceToSqr(vectors[i]); + + if (distSqr < minLength) { + minLength = distSqr; + index = i; + } + } + + return vectors[index]; + } + + public static Vec3 getNearestVector(Vec3 from, List vectors) { + return getNearestVector(from, vectors.toArray(new Vec3[0])); + } + + public static int greatest(int... iList) { + int max = Integer.MIN_VALUE; + + for (int i : iList) { + if (max < i) { + max = i; + } + } + + return max; + } + + public static int least(int... iList) { + int min = Integer.MAX_VALUE; + + for (int i : iList) { + if (min > i) { + min = i; + } + } + + return min; + } + + public static float greatest(float... fList) { + float max = -1000000.0F; + + for (float f : fList) { + if (max < f) { + max = f; + } + } + + return max; + } + + public static float least(float... fList) { + float min = 1000000.0F; + + for (float f : fList) { + if (min > f) { + min = f; + } + } + + return min; + } + + public static double greatest(double... dList) { + double max = -1000000.0D; + + for (double d : dList) { + if (max < d) { + max = d; + } + } + + return max; + } + + public static double least(double... dList) { + double min = 1000000.0D; + + for (double d : dList) { + if (min > d) { + min = d; + } + } + + return min; + } + + @Deprecated(forRemoval = true, since = "1.21.1") + public static void translateStack(PoseStack poseStack, OpenMatrix4f mat) { + poseStack.translate(mat.m30, mat.m31, mat.m32); + } + + private static final OpenMatrix4f OPEN_MATRIX_BUFFER = new OpenMatrix4f(); + + @Deprecated(forRemoval = true, since = "1.21.1") + public static void rotateStack(PoseStack poseStack, OpenMatrix4f mat) { + OpenMatrix4f.transpose(mat, OPEN_MATRIX_BUFFER); + poseStack.mulPose(getQuaternionFromMatrix(OPEN_MATRIX_BUFFER)); + } + + @Deprecated(forRemoval = true, since = "1.21.1") + public static void scaleStack(PoseStack poseStack, OpenMatrix4f mat) { + OpenMatrix4f.transpose(mat, OPEN_MATRIX_BUFFER); + Vector3f vector = getScaleVectorFromMatrix(OPEN_MATRIX_BUFFER); + poseStack.scale(vector.x(), vector.y(), vector.z()); + } + + private static final Matrix4f MATRIX4F = new Matrix4f(); + private static final Matrix3f MATRIX3F = new Matrix3f(); + + public static void mulStack(PoseStack poseStack, OpenMatrix4f mat) { + OpenMatrix4f.exportToMojangMatrix(mat, MATRIX4F); + MATRIX3F.set(MATRIX4F); + poseStack.mulPoseMatrix(MATRIX4F); + poseStack.last().normal().mul(MATRIX3F); + } + + public static double getAngleBetween(Vec3f a, Vec3f b) { + Vec3f normA = Vec3f.normalize(a, null); + Vec3f normB = Vec3f.normalize(b, null); + + double cos = (normA.x * normB.x + normA.y * normB.y + normA.z * normB.z); + return Math.toDegrees(Math.acos(cos)); + } + + public static double getAngleBetween(Vec3 a, Vec3 b) { + Vec3 normA = a.normalize(); + Vec3 normB = b.normalize(); + + double cos = (normA.x * normB.x + normA.y * normB.y + normA.z * normB.z); + return Math.toDegrees(Math.safeAcos(cos)); + } + + public static float getAngleBetween(Quaternionf a, Quaternionf b) { + float dot = a.w * b.w + a.x * b.x + a.y * b.y + a.z * b.z; + return 2.0F * (Math.safeAcos(MathUtils.getSign(dot) * b.w) - Math.safeAcos(a.w)); + } + + public static double getXRotOfVector(Vec3 vec) { + Vec3 normalized = vec.normalize(); + return -(Math.atan2(normalized.y, (float)Math.sqrt(normalized.x * normalized.x + normalized.z * normalized.z)) * (180D / Math.PI)); + } + + public static double getYRotOfVector(Vec3 vec) { + Vec3 normalized = vec.normalize(); + return Math.atan2(normalized.z, normalized.x) * (180D / Math.PI) - 90.0F; + } + + private static Quaternionf getQuaternionFromMatrix(OpenMatrix4f mat) { + Quaternionf quat = new Quaternionf(0, 0, 0, 1); + quat.setFromUnnormalized(OpenMatrix4f.exportToMojangMatrix(mat.transpose(null))); + return quat; + } + + public static Vec3f lerpVector(Vec3f start, Vec3f end, float delta) { + return lerpVector(start, end, delta, new Vec3f()); + } + + public static Vec3f lerpVector(Vec3f start, Vec3f end, float delta, Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + dest.x = start.x + (end.x - start.x) * delta; + dest.y = start.y + (end.y - start.y) * delta; + dest.z = start.z + (end.z - start.z) * delta; + + return dest; + } + + public static Vec3 lerpVector(Vec3 start, Vec3 end, float delta) { + return new Vec3(start.x + (end.x - start.x) * delta, start.y + (end.y - start.y) * delta, start.z + (end.z - start.z) * delta); + } + + public static Vector3f lerpMojangVector(Vector3f start, Vector3f end, float delta) { + float x = start.x() + (end.x() - start.x()) * delta; + float y = start.y() + (end.y() - start.y()) * delta; + float z = start.z() + (end.z() - start.z()) * delta; + return new Vector3f(x, y, z); + } + + public static Vec3 projectVector(Vec3 from, Vec3 to) { + double dot = to.dot(from); + double normalScale = 1.0D / ((to.x * to.x) + (to.y * to.y) + (to.z * to.z)); + + return new Vec3(dot * to.x * normalScale, dot * to.y * normalScale, dot * to.z * normalScale); + } + + public static Vec3f projectVector(Vec3f from, Vec3f to, Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + float dot = Vec3f.dot(to, from); + float normalScale = 1.0F / ((to.x * to.x) + (to.y * to.y) + (to.z * to.z)); + + dest.x = dot * to.x * normalScale; + dest.y = dot * to.y * normalScale; + dest.z = dot * to.z * normalScale; + + return dest; + } + + public static void setQuaternion(Quaternionf quat, float x, float y, float z, float w) { + quat.set(x, y, z, w); + } + + public static Quaternionf mulQuaternion(Quaternionf left, Quaternionf right, Quaternionf dest) { + if (dest == null) { + dest = new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F); + } + + float f = left.x(); + float f1 = left.y(); + float f2 = left.z(); + float f3 = left.w(); + float f4 = right.x(); + float f5 = right.y(); + float f6 = right.z(); + float f7 = right.w(); + float i = f3 * f4 + f * f7 + f1 * f6 - f2 * f5; + float j = f3 * f5 - f * f6 + f1 * f7 + f2 * f4; + float k = f3 * f6 + f * f5 - f1 * f4 + f2 * f7; + float r = f3 * f7 - f * f4 - f1 * f5 - f2 * f6; + + dest.set(i, j, k, r); + + return dest; + } + + public static Quaternionf lerpQuaternion(Quaternionf from, Quaternionf to, float delta) { + return lerpQuaternion(from, to, delta, null); + } + + public static Quaternionf lerpQuaternion(Quaternionf from, Quaternionf to, float delta, Quaternionf dest) { + if (dest == null) { + dest = new Quaternionf(); + } + + float fromX = from.x(); + float fromY = from.y(); + float fromZ = from.z(); + float fromW = from.w(); + float toX = to.x(); + float toY = to.y(); + float toZ = to.z(); + float toW = to.w(); + float resultX; + float resultY; + float resultZ; + float resultW; + float dot = fromW * toW + fromX * toX + fromY * toY + fromZ * toZ; + float blendI = 1.0F - delta; + + if (dot < 0.0F) { + resultW = blendI * fromW + delta * -toW; + resultX = blendI * fromX + delta * -toX; + resultY = blendI * fromY + delta * -toY; + resultZ = blendI * fromZ + delta * -toZ; + } else { + resultW = blendI * fromW + delta * toW; + resultX = blendI * fromX + delta * toX; + resultY = blendI * fromY + delta * toY; + resultZ = blendI * fromZ + delta * toZ; + } + + dest.set(resultX, resultY, resultZ, resultW); + dest.normalize(); + + return dest; + } + + private static Vector3f getScaleVectorFromMatrix(OpenMatrix4f mat) { + Vec3f a = new Vec3f(mat.m00, mat.m10, mat.m20); + Vec3f b = new Vec3f(mat.m01, mat.m11, mat.m21); + Vec3f c = new Vec3f(mat.m02, mat.m12, mat.m22); + return new Vector3f(a.length(), b.length(), c.length()); + } + + public static Set> getSubset(Collection collection) { + Set> subsets = new HashSet<> (); + List asList = new ArrayList<> (collection); + createSubset(0, asList, new HashSet<> (), subsets); + + return subsets; + } + + private static void createSubset(int idx, List elements, Set parent, Set> subsets) { + for (int i = idx; i < elements.size(); i++) { + Set subset = new HashSet<> (parent); + subset.add(elements.get(i)); + subsets.add(subset); + + createSubset(i + 1, elements, subset, subsets); + } + } + + public static int getLeastAngleVectorIdx(Vec3f src, Vec3f... candidates) { + int leastVectorIdx = -1; + int current = 0; + float maxDot = -10000.0F; + + for (Vec3f normzlizedVec : Stream.of(candidates).map((vec) -> vec.normalize()).collect(Collectors.toList())) { + float dot = Vec3f.dot(src, normzlizedVec); + + if (maxDot < dot) { + maxDot = dot; + leastVectorIdx = current; + } + + current++; + } + + return leastVectorIdx; + } + + public static Vec3f getLeastAngleVector(Vec3f src, Vec3f... candidates) { + return candidates[getLeastAngleVectorIdx(src, candidates)]; + } + + public static boolean canBeSeen(Entity target, Entity watcher, double maxDistance) { + if (target.level() != watcher.level()) { + return false; + } + + double sqr = maxDistance * maxDistance; + Level level = target.level(); + Vec3 vec1 = watcher.getEyePosition(); + + double height = target.getBoundingBox().maxY - target.getBoundingBox().minY; + Vec3 vec2 = target.position().add(0.0D, height * 0.15D, 0.0D); + Vec3 vec3 = target.position().add(0.0D, height * 0.5D, 0.0D); + Vec3 vec4 = target.position().add(0.0D, height * 0.95D, 0.0D); + + return vec1.distanceToSqr(vec2) < sqr && level.clip(new ClipContext(vec1, vec2, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, watcher)).getType() == HitResult.Type.MISS || + vec1.distanceToSqr(vec3) < sqr && level.clip(new ClipContext(vec1, vec3, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, watcher)).getType() == HitResult.Type.MISS || + vec1.distanceToSqr(vec4) < sqr && level.clip(new ClipContext(vec1, vec4, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, watcher)).getType() == HitResult.Type.MISS; + } + + public static int packColor(int r, int g, int b, int a) { + int ir = r << 16; + int ig = g << 8; + int ib = b; + int ia = a << 24; + + return ir | ig | ib | ia; + } + + public static void unpackColor(int packedColor, Vector4i result) { + int b = (packedColor & 0x000000FF); + int g = (packedColor & 0x0000FF00) >>> 8; + int r = (packedColor & 0x00FF0000) >>> 16; + int a = (packedColor & 0xFF000000) >>> 24; + + result.x = r; + result.y = g; + result.z = b; + result.w = a; + } + + public static byte normalIntValue(float pNum) { + return (byte)((int)(Mth.clamp(pNum, -1.0F, 1.0F) * 127.0F) & 255); + } + + /** + * Wrap a value within bounds + */ + public static int wrapClamp(int value, int min, int max) { + int stride = max - min + 1; + while (value < min) value += stride; + while (value > max) value -= stride; + return value; + } + + /** + * Transform the world coordinate system to -1~1 screen coord system + * @param projection current projection matrix + * @param modelView current model-view matrix + * @param position a source vector to transform + */ + public static Vec2 worldToScreenCoord(Matrix4f projectionMatrix, Camera camera, Vec3 position) { + Vector4f relativeCamera = new Vector4f((float)camera.getPosition().x() - (float)position.x(), (float)camera.getPosition().y() - (float)position.y(), (float)camera.getPosition().z() - (float)position.z(), 1.0F); + relativeCamera.rotate(Axis.YP.rotationDegrees(camera.getYRot() + 180.0F)); + relativeCamera.rotate(Axis.XP.rotationDegrees(camera.getXRot())); + relativeCamera.mul(projectionMatrix); + + float depth = relativeCamera.w; + relativeCamera.mul(1.0F / relativeCamera.w()); + + if (depth < 0.0F) { + relativeCamera.x = -relativeCamera.x; + relativeCamera.y = -relativeCamera.y; + } + + return new Vec2(relativeCamera.x(), relativeCamera.y()); + } + + private MathUtils() {} +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/math/MatrixOperation.java b/src/main/java/com/tiedup/remake/rig/math/MatrixOperation.java new file mode 100644 index 0000000..8cf46b6 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/MatrixOperation.java @@ -0,0 +1,12 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +@FunctionalInterface +public interface MatrixOperation { + OpenMatrix4f mul(OpenMatrix4f left, OpenMatrix4f right, OpenMatrix4f dest); +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/math/OpenMatrix4f.java b/src/main/java/com/tiedup/remake/rig/math/OpenMatrix4f.java new file mode 100644 index 0000000..5343c6e --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/OpenMatrix4f.java @@ -0,0 +1,953 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.List; + +import javax.annotation.Nullable; + +import org.joml.Math; +import org.joml.Matrix4f; +import org.joml.Quaternionf; + +import com.google.common.collect.Lists; + +import net.minecraft.world.phys.Vec3; +import com.tiedup.remake.rig.armature.JointTransform; + +public class OpenMatrix4f { + private static final FloatBuffer MATRIX_TRANSFORMER = ByteBuffer.allocateDirect(16 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + private static final Vec3f VECTOR_STORAGE = new Vec3f(); + private static final Vec4f VEC4_STORAGE = new Vec4f(); + + public static final OpenMatrix4f IDENTITY = new OpenMatrix4f(); + + /* + * m00 m01 m02 m03 + * m10 m11 m12 m13 + * m20 m21 m22 m23 + * m30 m31 m32 m33 + */ + public float m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33; + + private final boolean immutable; + + public OpenMatrix4f() { + this.setIdentity(); + this.immutable = false; + } + + public OpenMatrix4f(final OpenMatrix4f src) { + this(src, false); + } + + public OpenMatrix4f(final OpenMatrix4f src, boolean immutable) { + load(src); + this.immutable = immutable; + } + + public OpenMatrix4f(final JointTransform jointTransform) { + load(OpenMatrix4f.fromQuaternion(jointTransform.rotation()).translate(jointTransform.translation()).scale(jointTransform.scale())); + this.immutable = false; + } + + public OpenMatrix4f( + float m00, float m01, float m02, float m03 + , float m10, float m11, float m12, float m13 + , float m20, float m21, float m22, float m23 + , float m30, float m31, float m32, float m33 + ) { + this.m00 = m00; + this.m01 = m01; + this.m02 = m02; + this.m03 = m03; + this.m10 = m10; + this.m11 = m11; + this.m12 = m12; + this.m13 = m13; + this.m20 = m20; + this.m21 = m21; + this.m22 = m22; + this.m23 = m23; + this.m30 = m30; + this.m31 = m31; + this.m32 = m32; + this.m33 = m33; + this.immutable = false; + } + + public OpenMatrix4f setIdentity() { + return setIdentity(this); + } + + /** + * Set the given matrix to be the identity matrix. + * @param m The matrix to set to the identity + * @return m + */ + public static OpenMatrix4f setIdentity(OpenMatrix4f m) { + if (m.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + m.m00 = 1.0f; + m.m01 = 0.0f; + m.m02 = 0.0f; + m.m03 = 0.0f; + m.m10 = 0.0f; + m.m11 = 1.0f; + m.m12 = 0.0f; + m.m13 = 0.0f; + m.m20 = 0.0f; + m.m21 = 0.0f; + m.m22 = 1.0f; + m.m23 = 0.0f; + m.m30 = 0.0f; + m.m31 = 0.0f; + m.m32 = 0.0f; + m.m33 = 1.0f; + + return m; + } + + public OpenMatrix4f load(OpenMatrix4f src) { + return load(src, this); + } + + public static OpenMatrix4f load(OpenMatrix4f src, @Nullable OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + dest.m00 = src.m00; + dest.m01 = src.m01; + dest.m02 = src.m02; + dest.m03 = src.m03; + dest.m10 = src.m10; + dest.m11 = src.m11; + dest.m12 = src.m12; + dest.m13 = src.m13; + dest.m20 = src.m20; + dest.m21 = src.m21; + dest.m22 = src.m22; + dest.m23 = src.m23; + dest.m30 = src.m30; + dest.m31 = src.m31; + dest.m32 = src.m32; + dest.m33 = src.m33; + + return dest; + } + + public static OpenMatrix4f load(@Nullable OpenMatrix4f dest, float[] elements) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + dest.m00 = elements[0]; + dest.m01 = elements[1]; + dest.m02 = elements[2]; + dest.m03 = elements[3]; + dest.m10 = elements[4]; + dest.m11 = elements[5]; + dest.m12 = elements[6]; + dest.m13 = elements[7]; + dest.m20 = elements[8]; + dest.m21 = elements[9]; + dest.m22 = elements[10]; + dest.m23 = elements[11]; + dest.m30 = elements[12]; + dest.m31 = elements[13]; + dest.m32 = elements[14]; + dest.m33 = elements[15]; + + return dest; + } + + public static OpenMatrix4f load(@Nullable OpenMatrix4f dest, FloatBuffer buf) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + buf.position(0); + + dest.m00 = buf.get(); + dest.m01 = buf.get(); + dest.m02 = buf.get(); + dest.m03 = buf.get(); + dest.m10 = buf.get(); + dest.m11 = buf.get(); + dest.m12 = buf.get(); + dest.m13 = buf.get(); + dest.m20 = buf.get(); + dest.m21 = buf.get(); + dest.m22 = buf.get(); + dest.m23 = buf.get(); + dest.m30 = buf.get(); + dest.m31 = buf.get(); + dest.m32 = buf.get(); + dest.m33 = buf.get(); + + return dest; + } + + public OpenMatrix4f load(FloatBuffer buf) { + return OpenMatrix4f.load(this, buf); + } + + public OpenMatrix4f store(FloatBuffer buf) { + buf.put(m00); + buf.put(m01); + buf.put(m02); + buf.put(m03); + buf.put(m10); + buf.put(m11); + buf.put(m12); + buf.put(m13); + buf.put(m20); + buf.put(m21); + buf.put(m22); + buf.put(m23); + buf.put(m30); + buf.put(m31); + buf.put(m32); + buf.put(m33); + + return this; + } + + public List toList() { + List elements = Lists.newArrayList(); + + elements.add(0, m00); + elements.add(1, m01); + elements.add(2, m02); + elements.add(3, m03); + elements.add(4, m10); + elements.add(5, m11); + elements.add(6, m12); + elements.add(7, m13); + elements.add(8, m20); + elements.add(9, m21); + elements.add(10, m22); + elements.add(11, m23); + elements.add(12, m30); + elements.add(13, m31); + elements.add(14, m32); + elements.add(15, m33); + + return elements; + } + + public OpenMatrix4f unmodifiable() { + return new OpenMatrix4f(this, true); + } + + public static OpenMatrix4f add(OpenMatrix4f left, OpenMatrix4f right, @Nullable OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + dest.m00 = left.m00 + right.m00; + dest.m01 = left.m01 + right.m01; + dest.m02 = left.m02 + right.m02; + dest.m03 = left.m03 + right.m03; + dest.m10 = left.m10 + right.m10; + dest.m11 = left.m11 + right.m11; + dest.m12 = left.m12 + right.m12; + dest.m13 = left.m13 + right.m13; + dest.m20 = left.m20 + right.m20; + dest.m21 = left.m21 + right.m21; + dest.m22 = left.m22 + right.m22; + dest.m23 = left.m23 + right.m23; + dest.m30 = left.m30 + right.m30; + dest.m31 = left.m31 + right.m31; + dest.m32 = left.m32 + right.m32; + dest.m33 = left.m33 + right.m33; + + return dest; + } + + public OpenMatrix4f mulFront(OpenMatrix4f mulTransform) { + return OpenMatrix4f.mul(mulTransform, this, this); + } + + public OpenMatrix4f mulBack(OpenMatrix4f mulTransform) { + return OpenMatrix4f.mul(this, mulTransform, this); + } + + public static OpenMatrix4f mul(OpenMatrix4f left, OpenMatrix4f right, @Nullable OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + float m00 = left.m00 * right.m00 + left.m10 * right.m01 + left.m20 * right.m02 + left.m30 * right.m03; + float m01 = left.m01 * right.m00 + left.m11 * right.m01 + left.m21 * right.m02 + left.m31 * right.m03; + float m02 = left.m02 * right.m00 + left.m12 * right.m01 + left.m22 * right.m02 + left.m32 * right.m03; + float m03 = left.m03 * right.m00 + left.m13 * right.m01 + left.m23 * right.m02 + left.m33 * right.m03; + float m10 = left.m00 * right.m10 + left.m10 * right.m11 + left.m20 * right.m12 + left.m30 * right.m13; + float m11 = left.m01 * right.m10 + left.m11 * right.m11 + left.m21 * right.m12 + left.m31 * right.m13; + float m12 = left.m02 * right.m10 + left.m12 * right.m11 + left.m22 * right.m12 + left.m32 * right.m13; + float m13 = left.m03 * right.m10 + left.m13 * right.m11 + left.m23 * right.m12 + left.m33 * right.m13; + float m20 = left.m00 * right.m20 + left.m10 * right.m21 + left.m20 * right.m22 + left.m30 * right.m23; + float m21 = left.m01 * right.m20 + left.m11 * right.m21 + left.m21 * right.m22 + left.m31 * right.m23; + float m22 = left.m02 * right.m20 + left.m12 * right.m21 + left.m22 * right.m22 + left.m32 * right.m23; + float m23 = left.m03 * right.m20 + left.m13 * right.m21 + left.m23 * right.m22 + left.m33 * right.m23; + float m30 = left.m00 * right.m30 + left.m10 * right.m31 + left.m20 * right.m32 + left.m30 * right.m33; + float m31 = left.m01 * right.m30 + left.m11 * right.m31 + left.m21 * right.m32 + left.m31 * right.m33; + float m32 = left.m02 * right.m30 + left.m12 * right.m31 + left.m22 * right.m32 + left.m32 * right.m33; + float m33 = left.m03 * right.m30 + left.m13 * right.m31 + left.m23 * right.m32 + left.m33 * right.m33; + + dest.m00 = m00; + dest.m01 = m01; + dest.m02 = m02; + dest.m03 = m03; + dest.m10 = m10; + dest.m11 = m11; + dest.m12 = m12; + dest.m13 = m13; + dest.m20 = m20; + dest.m21 = m21; + dest.m22 = m22; + dest.m23 = m23; + dest.m30 = m30; + dest.m31 = m31; + dest.m32 = m32; + dest.m33 = m33; + + return dest; + } + + public static OpenMatrix4f mulMatrices(OpenMatrix4f... srcs) { + OpenMatrix4f result = new OpenMatrix4f(); + + for (OpenMatrix4f src : srcs) { + result.mulBack(src); + } + + return result; + } + + public static OpenMatrix4f mulAsOrigin(OpenMatrix4f left, OpenMatrix4f right, OpenMatrix4f dest) { + float x = right.m30; + float y = right.m31; + float z = right.m32; + + OpenMatrix4f result = mul(left, right, dest); + result.m30 = x; + result.m31 = y; + result.m32 = z; + + return result; + } + + public static OpenMatrix4f mulAsOriginInverse(OpenMatrix4f left, OpenMatrix4f right, OpenMatrix4f dest) { + return mulAsOrigin(right, left, dest); + } + + public static Vec4f transform(OpenMatrix4f matrix, Vec4f src, @Nullable Vec4f dest) { + if (dest == null) { + dest = new Vec4f(); + } + + float x = matrix.m00 * src.x + matrix.m10 * src.y + matrix.m20 * src.z + matrix.m30 * src.w; + float y = matrix.m01 * src.x + matrix.m11 * src.y + matrix.m21 * src.z + matrix.m31 * src.w; + float z = matrix.m02 * src.x + matrix.m12 * src.y + matrix.m22 * src.z + matrix.m32 * src.w; + float w = matrix.m03 * src.x + matrix.m13 * src.y + matrix.m23 * src.z + matrix.m33 * src.w; + + dest.x = x; + dest.y = y; + dest.z = z; + dest.w = w; + + return dest; + } + + public static Vec3 transform(OpenMatrix4f matrix, Vec3 src) { + double x = matrix.m00 * src.x + matrix.m10 * src.y + matrix.m20 * src.z + matrix.m30; + double y = matrix.m01 * src.x + matrix.m11 * src.y + matrix.m21 * src.z + matrix.m31; + double z = matrix.m02 * src.x + matrix.m12 * src.y + matrix.m22 * src.z + matrix.m32; + + return new Vec3(x, y ,z); + } + + public static Vec3f transform3v(OpenMatrix4f matrix, Vec3f src, @Nullable Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + VEC4_STORAGE.set(src.x, src.y, src.z, 1.0F); + + Vec4f result = transform(matrix, VEC4_STORAGE, null); + dest.x = result.x; + dest.y = result.y; + dest.z = result.z; + + return dest; + } + + public OpenMatrix4f transpose() { + return transpose(this); + } + + public OpenMatrix4f transpose(OpenMatrix4f dest) { + return transpose(this, dest); + } + + public static OpenMatrix4f transpose(OpenMatrix4f src, @Nullable OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + float m00 = src.m00; + float m01 = src.m10; + float m02 = src.m20; + float m03 = src.m30; + float m10 = src.m01; + float m11 = src.m11; + float m12 = src.m21; + float m13 = src.m31; + float m20 = src.m02; + float m21 = src.m12; + float m22 = src.m22; + float m23 = src.m32; + float m30 = src.m03; + float m31 = src.m13; + float m32 = src.m23; + float m33 = src.m33; + + dest.m00 = m00; + dest.m01 = m01; + dest.m02 = m02; + dest.m03 = m03; + dest.m10 = m10; + dest.m11 = m11; + dest.m12 = m12; + dest.m13 = m13; + dest.m20 = m20; + dest.m21 = m21; + dest.m22 = m22; + dest.m23 = m23; + dest.m30 = m30; + dest.m31 = m31; + dest.m32 = m32; + dest.m33 = m33; + + return dest; + } + + public float determinant() { + float f = m00 * ((m11 * m22 * m33 + m12 * m23 * m31 + m13 * m21 * m32) - m13 * m22 * m31 - m11 * m23 * m32 - m12 * m21 * m33); + f -= m01 * ((m10 * m22 * m33 + m12 * m23 * m30 + m13 * m20 * m32) - m13 * m22 * m30 - m10 * m23 * m32 - m12 * m20 * m33); + f += m02 * ((m10 * m21 * m33 + m11 * m23 * m30 + m13 * m20 * m31) - m13 * m21 * m30 - m10 * m23 * m31 - m11 * m20 * m33); + f -= m03 * ((m10 * m21 * m32 + m11 * m22 * m30 + m12 * m20 * m31) - m12 * m21 * m30 - m10 * m22 * m31 - m11 * m20 * m32); + + return f; + } + + private static float determinant3x3(float t00, float t01, float t02, float t10, float t11, float t12, float t20, float t21, float t22) { + return t00 * (t11 * t22 - t12 * t21) + t01 * (t12 * t20 - t10 * t22) + t02 * (t10 * t21 - t11 * t20); + } + + public OpenMatrix4f invert() { + return OpenMatrix4f.invert(this, this); + } + + public static OpenMatrix4f invert(OpenMatrix4f src, @Nullable OpenMatrix4f dest) { + float determinant = src.determinant(); + + if (determinant != 0.0F) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + float determinant_inv = 1.0F / determinant; + + float t00 = determinant3x3(src.m11, src.m12, src.m13, src.m21, src.m22, src.m23, src.m31, src.m32, src.m33); + float t01 = -determinant3x3(src.m10, src.m12, src.m13, src.m20, src.m22, src.m23, src.m30, src.m32, src.m33); + float t02 = determinant3x3(src.m10, src.m11, src.m13, src.m20, src.m21, src.m23, src.m30, src.m31, src.m33); + float t03 = -determinant3x3(src.m10, src.m11, src.m12, src.m20, src.m21, src.m22, src.m30, src.m31, src.m32); + float t10 = -determinant3x3(src.m01, src.m02, src.m03, src.m21, src.m22, src.m23, src.m31, src.m32, src.m33); + float t11 = determinant3x3(src.m00, src.m02, src.m03, src.m20, src.m22, src.m23, src.m30, src.m32, src.m33); + float t12 = -determinant3x3(src.m00, src.m01, src.m03, src.m20, src.m21, src.m23, src.m30, src.m31, src.m33); + float t13 = determinant3x3(src.m00, src.m01, src.m02, src.m20, src.m21, src.m22, src.m30, src.m31, src.m32); + float t20 = determinant3x3(src.m01, src.m02, src.m03, src.m11, src.m12, src.m13, src.m31, src.m32, src.m33); + float t21 = -determinant3x3(src.m00, src.m02, src.m03, src.m10, src.m12, src.m13, src.m30, src.m32, src.m33); + float t22 = determinant3x3(src.m00, src.m01, src.m03, src.m10, src.m11, src.m13, src.m30, src.m31, src.m33); + float t23 = -determinant3x3(src.m00, src.m01, src.m02, src.m10, src.m11, src.m12, src.m30, src.m31, src.m32); + float t30 = -determinant3x3(src.m01, src.m02, src.m03, src.m11, src.m12, src.m13, src.m21, src.m22, src.m23); + float t31 = determinant3x3(src.m00, src.m02, src.m03, src.m10, src.m12, src.m13, src.m20, src.m22, src.m23); + float t32 = -determinant3x3(src.m00, src.m01, src.m03, src.m10, src.m11, src.m13, src.m20, src.m21, src.m23); + float t33 = determinant3x3(src.m00, src.m01, src.m02, src.m10, src.m11, src.m12, src.m20, src.m21, src.m22); + + dest.m00 = t00 * determinant_inv; + dest.m11 = t11 * determinant_inv; + dest.m22 = t22 * determinant_inv; + dest.m33 = t33 * determinant_inv; + dest.m01 = t10 * determinant_inv; + dest.m10 = t01 * determinant_inv; + dest.m20 = t02 * determinant_inv; + dest.m02 = t20 * determinant_inv; + dest.m12 = t21 * determinant_inv; + dest.m21 = t12 * determinant_inv; + dest.m03 = t30 * determinant_inv; + dest.m30 = t03 * determinant_inv; + dest.m13 = t31 * determinant_inv; + dest.m31 = t13 * determinant_inv; + dest.m32 = t23 * determinant_inv; + dest.m23 = t32 * determinant_inv; + + return dest; + } else { + dest.setIdentity(); + return dest; + } + } + + public OpenMatrix4f translate(float x, float y, float z) { + VECTOR_STORAGE.set(x, y, z); + return translate(VECTOR_STORAGE, this); + } + + public OpenMatrix4f translate(Vec3f vec) { + return translate(vec, this); + } + + public OpenMatrix4f translate(Vec3f vec, OpenMatrix4f dest) { + return translate(vec, this, dest); + } + + public static OpenMatrix4f translate(Vec3f vec, OpenMatrix4f src, @Nullable OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + dest.m30 += src.m00 * vec.x + src.m10 * vec.y + src.m20 * vec.z; + dest.m31 += src.m01 * vec.x + src.m11 * vec.y + src.m21 * vec.z; + dest.m32 += src.m02 * vec.x + src.m12 * vec.y + src.m22 * vec.z; + dest.m33 += src.m03 * vec.x + src.m13 * vec.y + src.m23 * vec.z; + + return dest; + } + + public static OpenMatrix4f createTranslation(float x, float y, float z) { + return ofTranslation(x, y, z, null); + } + + public static OpenMatrix4f ofTranslation(float x, float y, float z, OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + dest.setIdentity(); + dest.m30 = x; + dest.m31 = y; + dest.m32 = z; + + return dest; + } + + public static OpenMatrix4f createScale(float x, float y, float z) { + return ofScale(x, y, z, null); + } + + public static OpenMatrix4f ofScale(float x, float y, float z, OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + dest.setIdentity(); + dest.m00 = x; + dest.m11 = y; + dest.m22 = z; + + return dest; + } + + public OpenMatrix4f rotateDeg(float angle, Vec3f axis) { + return rotate((float)Math.toRadians(angle), axis); + } + + public OpenMatrix4f rotate(float angle, Vec3f axis) { + return rotate(angle, axis, this); + } + + public OpenMatrix4f rotate(float angle, Vec3f axis, OpenMatrix4f dest) { + return rotate(angle, axis, this, dest); + } + + public static OpenMatrix4f createRotatorDeg(float degree, Vec3f axis) { + return rotate((float)Math.toRadians(degree), axis, new OpenMatrix4f(), null); + } + + public static OpenMatrix4f ofRotationDegree(float degree, Vec3f axis, @Nullable OpenMatrix4f dest) { + dest.setIdentity(); + return rotate((float)Math.toRadians(degree), axis, dest, dest); + } + + public static OpenMatrix4f rotate(float angle, Vec3f axis, OpenMatrix4f src, @Nullable OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + float c = (float) Math.cos(angle); + float s = (float) Math.sin(angle); + float oneminusc = 1.0f - c; + float xy = axis.x * axis.y; + float yz = axis.y * axis.z; + float xz = axis.x * axis.z; + float xs = axis.x * s; + float ys = axis.y * s; + float zs = axis.z * s; + + float f00 = axis.x * axis.x * oneminusc+c; + float f01 = xy * oneminusc + zs; + float f02 = xz * oneminusc - ys; + // n[3] not used + float f10 = xy * oneminusc - zs; + float f11 = axis.y * axis.y * oneminusc+c; + float f12 = yz * oneminusc + xs; + // n[7] not used + float f20 = xz * oneminusc + ys; + float f21 = yz * oneminusc - xs; + float f22 = axis.z * axis.z * oneminusc+c; + + float t00 = src.m00 * f00 + src.m10 * f01 + src.m20 * f02; + float t01 = src.m01 * f00 + src.m11 * f01 + src.m21 * f02; + float t02 = src.m02 * f00 + src.m12 * f01 + src.m22 * f02; + float t03 = src.m03 * f00 + src.m13 * f01 + src.m23 * f02; + float t10 = src.m00 * f10 + src.m10 * f11 + src.m20 * f12; + float t11 = src.m01 * f10 + src.m11 * f11 + src.m21 * f12; + float t12 = src.m02 * f10 + src.m12 * f11 + src.m22 * f12; + float t13 = src.m03 * f10 + src.m13 * f11 + src.m23 * f12; + + dest.m20 = src.m00 * f20 + src.m10 * f21 + src.m20 * f22; + dest.m21 = src.m01 * f20 + src.m11 * f21 + src.m21 * f22; + dest.m22 = src.m02 * f20 + src.m12 * f21 + src.m22 * f22; + dest.m23 = src.m03 * f20 + src.m13 * f21 + src.m23 * f22; + dest.m00 = t00; + dest.m01 = t01; + dest.m02 = t02; + dest.m03 = t03; + dest.m10 = t10; + dest.m11 = t11; + dest.m12 = t12; + dest.m13 = t13; + + return dest; + } + + public Vec3f toTranslationVector() { + return toTranslationVector(this); + } + + public Vec3f toTranslationVector(Vec3f dest) { + return toTranslationVector(this, dest); + } + + public static Vec3f toTranslationVector(OpenMatrix4f matrix) { + return toTranslationVector(matrix, null); + } + + public static Vec3f toTranslationVector(OpenMatrix4f matrix, Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + dest.x = matrix.m30; + dest.y = matrix.m31; + dest.z = matrix.m32; + + return dest; + } + + public Quaternionf toQuaternion() { + return OpenMatrix4f.toQuaternion(this); + } + + public Quaternionf toQuaternion(Quaternionf dest) { + return OpenMatrix4f.toQuaternion(this, dest); + } + + public static Quaternionf toQuaternion(OpenMatrix4f matrix) { + return toQuaternion(matrix, new Quaternionf()); + } + + private static final OpenMatrix4f MATRIX_STORAGE = new OpenMatrix4f(); + + public static Quaternionf toQuaternion(OpenMatrix4f matrix, Quaternionf dest) { + if (dest == null) { + dest = new Quaternionf(); + } + + OpenMatrix4f.load(matrix, MATRIX_STORAGE); + + float w, x, y, z; + MATRIX_STORAGE.transpose(); + + float lenX = MATRIX_STORAGE.m00 * MATRIX_STORAGE.m00 + MATRIX_STORAGE.m01 * MATRIX_STORAGE.m01 + MATRIX_STORAGE.m02 * MATRIX_STORAGE.m02; + float lenY = MATRIX_STORAGE.m10 * MATRIX_STORAGE.m10 + MATRIX_STORAGE.m11 * MATRIX_STORAGE.m11 + MATRIX_STORAGE.m12 * MATRIX_STORAGE.m12; + float lenZ = MATRIX_STORAGE.m20 * MATRIX_STORAGE.m20 + MATRIX_STORAGE.m21 * MATRIX_STORAGE.m21 + MATRIX_STORAGE.m22 * MATRIX_STORAGE.m22; + + if (lenX == 0.0F || lenY == 0.0F || lenZ == 0.0F) { + return new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F); + } + + lenX = Math.invsqrt(lenX); + lenY = Math.invsqrt(lenY); + lenZ = Math.invsqrt(lenZ); + + MATRIX_STORAGE.m00 *= lenX; MATRIX_STORAGE.m01 *= lenX; MATRIX_STORAGE.m02 *= lenX; + MATRIX_STORAGE.m10 *= lenY; MATRIX_STORAGE.m11 *= lenY; MATRIX_STORAGE.m12 *= lenY; + MATRIX_STORAGE.m20 *= lenZ; MATRIX_STORAGE.m21 *= lenZ; MATRIX_STORAGE.m22 *= lenZ; + + float t; + float tr = MATRIX_STORAGE.m00 + MATRIX_STORAGE.m11 + MATRIX_STORAGE.m22; + + if (tr >= 0.0F) { + t = (float)Math.sqrt(tr + 1.0F); + w = t * 0.5F; + t = 0.5F / t; + x = (MATRIX_STORAGE.m12 - MATRIX_STORAGE.m21) * t; + y = (MATRIX_STORAGE.m20 - MATRIX_STORAGE.m02) * t; + z = (MATRIX_STORAGE.m01 - MATRIX_STORAGE.m10) * t; + } else { + if (MATRIX_STORAGE.m00 >= MATRIX_STORAGE.m11 && MATRIX_STORAGE.m00 >= MATRIX_STORAGE.m22) { + t = (float)Math.sqrt(MATRIX_STORAGE.m00 - (MATRIX_STORAGE.m11 + MATRIX_STORAGE.m22) + 1.0); + x = t * 0.5F; + t = 0.5F / t; + y = (MATRIX_STORAGE.m10 + MATRIX_STORAGE.m01) * t; + z = (MATRIX_STORAGE.m02 + MATRIX_STORAGE.m20) * t; + w = (MATRIX_STORAGE.m12 - MATRIX_STORAGE.m21) * t; + } else if (MATRIX_STORAGE.m11 > MATRIX_STORAGE.m22) { + t = (float)Math.sqrt(MATRIX_STORAGE.m11 - (MATRIX_STORAGE.m22 + MATRIX_STORAGE.m00) + 1.0F); + y = t * 0.5F; + t = 0.5F / t; + z = (MATRIX_STORAGE.m21 + MATRIX_STORAGE.m12) * t; + x = (MATRIX_STORAGE.m10 + MATRIX_STORAGE.m01) * t; + w = (MATRIX_STORAGE.m20 - MATRIX_STORAGE.m02) * t; + } else { + t = (float)Math.sqrt(MATRIX_STORAGE.m22 - (MATRIX_STORAGE.m00 + MATRIX_STORAGE.m11) + 1.0F); + z = t * 0.5F; + t = 0.5F / t; + x = (MATRIX_STORAGE.m02 + MATRIX_STORAGE.m20) * t; + y = (MATRIX_STORAGE.m21 + MATRIX_STORAGE.m12) * t; + w = (MATRIX_STORAGE.m01 - MATRIX_STORAGE.m10) * t; + } + } + + dest.x = x; + dest.y = y; + dest.z = z; + dest.w = w; + + return dest; + } + + public static OpenMatrix4f fromQuaternion(Quaternionf quaternion) { + return fromQuaternion(quaternion, null); + } + + public static OpenMatrix4f fromQuaternion(Quaternionf quaternion, OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + float x = quaternion.x(); + float y = quaternion.y(); + float z = quaternion.z(); + float w = quaternion.w(); + float xy = x * y; + float xz = x * z; + float xw = x * w; + float yz = y * z; + float yw = y * w; + float zw = z * w; + float xSquared = 2F * x * x; + float ySquared = 2F * y * y; + float zSquared = 2F * z * z; + dest.m00 = 1.0F - ySquared - zSquared; + dest.m01 = 2.0F * (xy - zw); + dest.m02 = 2.0F * (xz + yw); + dest.m10 = 2.0F * (xy + zw); + dest.m11 = 1.0F - xSquared - zSquared; + dest.m12 = 2.0F * (yz - xw); + dest.m20 = 2.0F * (xz - yw); + dest.m21 = 2.0F * (yz + xw); + dest.m22 = 1.0F - xSquared - ySquared; + + return dest; + } + + public OpenMatrix4f scale(float x, float y, float z) { + VECTOR_STORAGE.set(x, y, z); + return this.scale(VECTOR_STORAGE); + } + + public OpenMatrix4f scale(Vec3f vec) { + return scale(vec, this, this); + } + + public static OpenMatrix4f scale(Vec3f vec, OpenMatrix4f src, @Nullable OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + dest.m00 = src.m00 * vec.x; + dest.m01 = src.m01 * vec.x; + dest.m02 = src.m02 * vec.x; + dest.m03 = src.m03 * vec.x; + dest.m10 = src.m10 * vec.y; + dest.m11 = src.m11 * vec.y; + dest.m12 = src.m12 * vec.y; + dest.m13 = src.m13 * vec.y; + dest.m20 = src.m20 * vec.z; + dest.m21 = src.m21 * vec.z; + dest.m22 = src.m22 * vec.z; + dest.m23 = src.m23 * vec.z; + + return dest; + } + + public Vec3f toScaleVector() { + return toScaleVector(null); + } + + public Vec3f toScaleVector(Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + VECTOR_STORAGE.set(this.m00, this.m01, this.m02); + dest.x = VECTOR_STORAGE.length(); + + VECTOR_STORAGE.set(this.m10, this.m11, this.m12); + dest.y = VECTOR_STORAGE.length(); + + VECTOR_STORAGE.set(this.m20, this.m21, this.m22); + dest.z = VECTOR_STORAGE.length(); + + return dest; + } + + public OpenMatrix4f removeTranslation() { + return removeTranslation(this, null); + } + + public static OpenMatrix4f removeTranslation(OpenMatrix4f src, @Nullable OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + dest.load(src); + dest.m30 = 0.0F; + dest.m31 = 0.0F; + dest.m32 = 0.0F; + + return dest; + } + + public OpenMatrix4f removeScale() { + return removeScale(this, null); + } + + public static OpenMatrix4f removeScale(OpenMatrix4f src, @Nullable OpenMatrix4f dest) { + if (dest == null) { + dest = new OpenMatrix4f(); + } else if (dest.immutable) { + throw new UnsupportedOperationException("Can't modify immutable matrix"); + } + + VECTOR_STORAGE.set(src.m00, src.m01, src.m02); + float xScale = VECTOR_STORAGE.length(); + + VECTOR_STORAGE.set(src.m10, src.m11, src.m12); + float yScale = VECTOR_STORAGE.length(); + + VECTOR_STORAGE.set(src.m20, src.m21, src.m22); + float zScale = VECTOR_STORAGE.length(); + + dest.load(src); + dest.scale(1.0F / xScale, 1.0F / yScale, 1.0F / zScale); + + return dest; + } + + @Override + public String toString() { + return "\n" + + String.format("%.4f", m00) + " " + String.format("%.4f", m01) + " " + String.format("%.4f", m02) + " " + String.format("%.4f", m03) + "\n" + + String.format("%.4f", m10) + " " + String.format("%.4f", m11) + " " + String.format("%.4f", m12) + " " + String.format("%.4f", m13) + "\n" + + String.format("%.4f", m20) + " " + String.format("%.4f", m21) + " " + String.format("%.4f", m22) + " " + String.format("%.4f", m23) + "\n" + + String.format("%.4f", m30) + " " + String.format("%.4f", m31) + " " + String.format("%.4f", m32) + " " + String.format("%.4f", m33) + "\n" + ; + } + + public static Matrix4f exportToMojangMatrix(OpenMatrix4f src) { + return exportToMojangMatrix(src, null); + } + + public static Matrix4f exportToMojangMatrix(OpenMatrix4f src, Matrix4f dest) { + if (dest == null) { + dest = new Matrix4f(); + } + + MATRIX_TRANSFORMER.position(0); + src.store(MATRIX_TRANSFORMER); + MATRIX_TRANSFORMER.position(0); + + return dest.set(MATRIX_TRANSFORMER); + } + + public static OpenMatrix4f importFromMojangMatrix(Matrix4f src) { + MATRIX_TRANSFORMER.position(0); + src.get(MATRIX_TRANSFORMER); + + return OpenMatrix4f.load(null, MATRIX_TRANSFORMER); + } + + public static OpenMatrix4f[] allocateMatrixArray(int size) { + OpenMatrix4f[] matrixArray = new OpenMatrix4f[size]; + + for (int i = 0; i < size; i++) { + matrixArray[i] = new OpenMatrix4f(); + } + + return matrixArray; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/math/QuaternionUtils.java b/src/main/java/com/tiedup/remake/rig/math/QuaternionUtils.java new file mode 100644 index 0000000..310653c --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/QuaternionUtils.java @@ -0,0 +1,46 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +import org.joml.Quaternionf; +import org.joml.Vector3f; + +public class QuaternionUtils { + public static Axis XN = new Axis(-1.0F, 0.0F, 0.0F); + public static Axis XP = new Axis(1.0F, 0.0F, 0.0F); + public static Axis YN = new Axis(0.0F, -1.0F, 0.0F); + public static Axis YP = new Axis(0.0F, 1.0F, 0.0F); + public static Axis ZN = new Axis(0.0F, 0.0F, -1.0F); + public static Axis ZP = new Axis(0.0F, 0.0F, 1.0F); + + public static Quaternionf rotationDegrees(Vector3f axis, float degress) { + float angle = degress * (float) Math.PI / 180; + return rotation(axis, angle); + } + + public static Quaternionf rotation(Vector3f axis, float angle) { + Quaternionf quat = new Quaternionf(); + quat.setAngleAxis(angle, axis.x, axis.y, axis.z); + return quat; + } + + public static class Axis { + private final Vector3f axis; + + public Axis(float x, float y, float z) { + this.axis = new Vector3f(x, y, z); + } + + public Quaternionf rotation(float angle) { + return QuaternionUtils.rotation(axis, angle); + } + + public Quaternionf rotationDegrees(float degrees) { + return QuaternionUtils.rotationDegrees(axis, degrees); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/math/ValueModifier.java b/src/main/java/com/tiedup/remake/rig/math/ValueModifier.java new file mode 100644 index 0000000..dcf05a7 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/ValueModifier.java @@ -0,0 +1,139 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +public interface ValueModifier { + public static final Codec CODEC = + RecordCodecBuilder.create(instance -> instance.group( + Codec.FLOAT.fieldOf("adder").forGetter(Unified::adder), + Codec.FLOAT.fieldOf("multiplier").forGetter(Unified::multiplier), + Codec.FLOAT.fieldOf("setter").forGetter(Unified::setter) + ).apply(instance, Unified::new) + ); + + public void attach(ResultCalculator calculator); + + public static ValueModifier adder(float value) { + return new Adder(value); + } + + public static ValueModifier multiplier(float value) { + return new Multiplier(value); + } + + public static ValueModifier setter(float arg) { + return new Setter(arg); + } + + public static record Adder(float adder) implements ValueModifier { + @Override + public void attach(ResultCalculator calculator) { + calculator.add += this.adder; + } + } + + public static record Multiplier(float multiplier) implements ValueModifier { + @Override + public void attach(ResultCalculator calculator) { + calculator.multiply *= this.multiplier; + } + } + + public static record Setter(float setter) implements ValueModifier { + @Override + public void attach(ResultCalculator calculator) { + if (Float.isNaN(calculator.set)) { + calculator.set = this.setter; + } else if (!Float.isNaN(this.setter)) { + calculator.set = Math.min(calculator.set, this.setter); + } + } + } + + public static record Unified(float adder, float multiplier, float setter) implements ValueModifier { + @Override + public void attach(ResultCalculator calculator) { + if (Float.isNaN(calculator.set)) { + calculator.set = this.setter; + } else if (!Float.isNaN(this.setter)) { + calculator.set = Math.min(calculator.set, this.setter); + } + + calculator.add += this.adder; + calculator.multiply *= this.multiplier; + } + } + + public static ResultCalculator calculator() { + return new ResultCalculator(); + } + + public static class ResultCalculator implements ValueModifier { + private float set = Float.NaN; + private float add = 0.0F; + private float multiply = 1.0F; + + public ResultCalculator attach(ValueModifier valueModifier) { + valueModifier.attach(this); + return this; + } + + @Override + public void attach(ResultCalculator calculator) { + if (Float.isNaN(calculator.set)) { + calculator.set = this.set; + } else if (!Float.isNaN(this.set)) { + calculator.set = Math.min(calculator.set, this.set); + } + + calculator.add += this.add; + calculator.multiply *= this.multiply; + } + + public ValueModifier toValueModifier() { + if (Float.isNaN(this.set)) { + if (Float.compare(this.add, 0.0F) == 0 && Float.compare(this.multiply, 1.0F) != 0) { + return new Multiplier(this.multiply); + } else if (Float.compare(this.add, 0.0F) != 0 && Float.compare(this.multiply, 1.0F) == 0) { + return new Adder(this.add); + } + } else if (Float.compare(this.add, 0.0F) == 0 && Float.compare(this.multiply, 1.0F) == 0) { + return new Setter(this.set); + } + + return new Unified(this.set, this.add, this.multiply); + } + + public void set(float f) { + this.set = f; + } + + public void add(float f) { + this.add += add; + } + + public void multiply(float f) { + this.multiply *= f; + } + + public float getResult(float baseValue) { + float result = baseValue; + + if (!Float.isNaN(this.set)) { + result = this.set; + } + + result += this.add; + result *= this.multiply; + + return result; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/math/Vec2f.java b/src/main/java/com/tiedup/remake/rig/math/Vec2f.java new file mode 100644 index 0000000..031950d --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/Vec2f.java @@ -0,0 +1,33 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +public class Vec2f { + public float x; + public float y; + + public Vec2f() { + this.x = 0; + this.y = 0; + } + + public Vec2f(float x, float y) { + this.x = x; + this.y = y; + } + + public Vec2f scale(float f) { + this.x *= f; + this.y *= f; + return this; + } + + @Override + public String toString() { + return "Vec2f[" + this.x + ", " + this.y + ", " + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/math/Vec2i.java b/src/main/java/com/tiedup/remake/rig/math/Vec2i.java new file mode 100644 index 0000000..0c292d9 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/Vec2i.java @@ -0,0 +1,22 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +public class Vec2i { + public int x; + public int y; + + public Vec2i(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + public String toString() { + return String.format("[%d, %d]", this.x, this.y); + } +} diff --git a/src/main/java/com/tiedup/remake/rig/math/Vec3f.java b/src/main/java/com/tiedup/remake/rig/math/Vec3f.java new file mode 100644 index 0000000..6a037fc --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/Vec3f.java @@ -0,0 +1,473 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +import java.util.Collection; +import java.util.List; + +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import net.minecraft.world.phys.Vec3; +import yesman.epicfight.main.EpicFightMod; + +public class Vec3f extends Vec2f { + public static final Vec3f X_AXIS = new Vec3f(1.0F, 0.0F, 0.0F); + public static final Vec3f Y_AXIS = new Vec3f(0.0F, 1.0F, 0.0F); + public static final Vec3f Z_AXIS = new Vec3f(0.0F, 0.0F, 1.0F); + public static final Vec3f M_X_AXIS = new Vec3f(-1.0F, 0.0F, 0.0F); + public static final Vec3f M_Y_AXIS = new Vec3f(0.0F, -1.0F, 0.0F); + public static final Vec3f M_Z_AXIS = new Vec3f(0.0F, 0.0F, -1.0F); + public static final Vec3f ZERO = new Vec3f(0.0F, 0.0F, 0.0F); + + public float z; + + public Vec3f() { + super(); + this.z = 0; + } + + public Vec3f(float x, float y, float z) { + super(x, y); + this.z = z; + } + + public Vec3f(double x, double y, double z) { + this((float)x, (float)y, (float)z); + } + + public Vec3f(Vec3 mojangVec) { + this((float)mojangVec.x, (float)mojangVec.y, (float)mojangVec.z); + } + + public Vec3f set(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + public Vec3f set(Vec3 vec3f) { + this.x = (float)vec3f.x; + this.y = (float)vec3f.y; + this.z = (float)vec3f.z; + return this; + } + + public Vec3f set(Vec3f vec3f) { + this.x = vec3f.x; + this.y = vec3f.y; + this.z = vec3f.z; + return this; + } + + public Vec3f add(float x, float y, float z) { + this.x += x; + this.y += y; + this.z += z; + return this; + } + + public Vec3f add(Vec3f vec) { + return this.add(vec.x, vec.y, vec.z); + } + + public Vec3f add(Vec3 vec) { + return this.add((float)vec.x, (float)vec.y, (float)vec.z); + } + + public Vec3f sub(float x, float y, float z) { + this.x -= x; + this.y -= y; + this.z -= z; + return this; + } + + public Vec3f sub(Vec3f vec) { + return this.sub(vec.x, vec.y, vec.z); + } + + public static Vec3f add(Vec3f left, Vec3f right, Vec3f dest) { + if (dest == null) { + return new Vec3f(left.x + right.x, left.y + right.y, left.z + right.z); + } else { + dest.set(left.x + right.x, left.y + right.y, left.z + right.z); + return dest; + } + } + + public static Vec3f sub(Vec3f left, Vec3f right, Vec3f dest) { + if (dest == null) { + return new Vec3f(left.x - right.x, left.y - right.y, left.z - right.z); + } else { + dest.set(left.x - right.x, left.y - right.y, left.z - right.z); + return dest; + } + } + + public Vec3f multiply(Vec3f vec) { + return multiply(this, this, vec.x, vec.y, vec.z); + } + + public Vec3f multiply(float x, float y, float z) { + return multiply(this, this, x, y, z); + } + + public static Vec3f multiply(Vec3f src, Vec3f dest, float x, float y, float z) { + if (dest == null) { + dest = new Vec3f(); + } + + dest.x = src.x * x; + dest.y = src.y * y; + dest.z = src.z * z; + + return dest; + } + + @Override + public Vec3f scale(float f) { + return scale(this, this, f); + } + + public static Vec3f scale(Vec3f src, Vec3f dest, float f) { + if (dest == null) { + dest = new Vec3f(); + } + + dest.x = src.x * f; + dest.y = src.y * f; + dest.z = src.z * f; + + return dest; + } + + public Vec3f copy() { + return new Vec3f(this.x, this.y, this.z); + } + + public float length() { + return (float) Math.sqrt(this.lengthSqr()); + } + + public float lengthSqr() { + return this.x * this.x + this.y * this.y + this.z * this.z; + } + + public float distance(Vec3f opponent) { + return (float)Math.sqrt(this.distanceSqr(opponent)); + } + + public float distanceSqr(Vec3f opponent) { + return (float)(Math.pow(this.x - opponent.x, 2) + Math.pow(this.y - opponent.y, 2) + Math.pow(this.z - opponent.z, 2)); + } + + public float horizontalDistance() { + return (float)Math.sqrt(this.x * this.x + this.z * this.z); + } + + public float horizontalDistanceSqr() { + return this.x * this.x + this.z * this.z; + } + + public void rotate(float degree, Vec3f axis) { + rotate(degree, axis, this, this); + } + + public void invalidate() { + this.x = Float.NaN; + this.y = Float.NaN; + this.z = Float.NaN; + } + + public boolean validateValues() { + return Float.isFinite(this.x) && Float.isFinite(this.y) && Float.isFinite(this.z); + } + + public static Vec3f rotate(float degree, Vec3f axis, Vec3f src, Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + return OpenMatrix4f.transform3v(OpenMatrix4f.createRotatorDeg(degree, axis), src, dest); + } + + private static final Vector3f SRC = new Vector3f(); + private static final Vector3f TRANSFORM_RESULT = new Vector3f(); + + public static Vec3f rotate(Quaternionf rot, Vec3f src, Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + SRC.set(src.x, src.y, src.z); + rot.transform(SRC, TRANSFORM_RESULT); + dest.set(TRANSFORM_RESULT.x, TRANSFORM_RESULT.y, TRANSFORM_RESULT.z); + + return dest; + } + + public static float dot(Vec3f left, Vec3f right) { + return left.x * right.x + left.y * right.y + left.z * right.z; + } + + public static Vec3f cross(Vec3f left, Vec3f right, Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + dest.set(left.y * right.z - left.z * right.y, right.x * left.z - right.z * left.x, left.x * right.y - left.y * right.x); + + return dest; + } + + public static float getAngleBetween(Vec3f a, Vec3f b) { + return (float) Math.acos(Math.min(1.0F, Vec3f.dot(a, b) / (a.length() * b.length()))); + } + + public static Quaternionf getRotatorBetween(Vec3f a, Vec3f b, Quaternionf dest) { + if (dest == null) { + dest = new Quaternionf(); + } + + Vec3f axis = Vec3f.cross(a, b, null).normalize(); + float dotDivLength = Vec3f.dot(a, b) / (a.length() * b.length()); + + if (!Float.isFinite(dotDivLength)) { + EpicFightMod.LOGGER.info("Warning : given vector's length is zero"); + (new IllegalArgumentException()).printStackTrace(); + dotDivLength = 1.0F; + } + + float radian = (float)Math.acos(Math.min(1.0F, dotDivLength)); + dest.setAngleAxis(radian, axis.x, axis.y, axis.z); + + return dest; + } + + public static Vec3f interpolate(Vec3f from, Vec3f to, float interpolation, Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + dest.x = from.x + (to.x - from.x) * interpolation; + dest.y = from.y + (to.y - from.y) * interpolation; + dest.z = from.z + (to.z - from.z) * interpolation; + + return dest; + } + + public Vec3f normalize() { + return normalize(this, this); + } + + public static Vec3f normalize(Vec3f src, Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + float norm = (float) Math.sqrt(src.x * src.x + src.y * src.y + src.z * src.z); + + if (norm > 1E-5F) { + dest.x = src.x / norm; + dest.y = src.y / norm; + dest.z = src.z / norm; + } else { + dest.x = 0; + dest.y = 0; + dest.z = 0; + } + + return dest; + } + + @Override + public String toString() { + return "[" + this.x + ", " + this.y + ", " + this.z + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o instanceof Vec3f vec3f) { + return Float.compare(this.x, vec3f.x) == 0 && Float.compare(this.y, vec3f.y) == 0 && Float.compare(this.z, vec3f.z) == 0; + } + + return false; + } + + @Override + public int hashCode() { + int j = Float.floatToIntBits(this.x); + int i = (int) (j ^ j >>> 32); + j = Float.floatToIntBits(this.y); + i = 31 * i + (int) (j ^ j >>> 32); + j = Float.floatToIntBits(this.z); + + return 31 * i + (int) (j ^ j >>> 32); + } + + public static Vec3f average(Collection vectors, Vec3f dest) { + if (dest == null) { + dest = new Vec3f(); + } + + dest.set(0.0F, 0.0F, 0.0F); + + for (Vec3f v : vectors) { + dest.add(v); + } + + dest.scale(1.0F / vectors.size()); + + return dest; + } + + public static Vec3f average(Vec3f dest, Vec3f... vectors) { + if (dest == null) { + dest = new Vec3f(); + } + + dest.set(0.0F, 0.0F, 0.0F); + + for (Vec3f v : vectors) { + dest.add(v); + } + + dest.scale(vectors.length); + + return dest; + } + + public static int getNearest(Vec3f from, List vectors) { + float minLength = Float.MAX_VALUE; + int index = -1; + + for (int i = 0; i < vectors.size(); i++) { + if (vectors.get(i) == null) { + continue; + } + + if (!vectors.get(i).validateValues()) { + continue; + } + + float distSqr = from.distanceSqr(vectors.get(i)); + + if (distSqr < minLength) { + minLength = distSqr; + index = i; + } + } + + return index; + } + + public static int getNearest(Vec3f from, Vec3f... vectors) { + float minLength = Float.MAX_VALUE; + int index = -1; + + for (int i = 0; i < vectors.length; i++) { + if (vectors[i] == null) { + continue; + } + + if (!vectors[i].validateValues()) { + continue; + } + + float distSqr = from.distanceSqr(vectors[i]); + + if (distSqr < minLength) { + minLength = distSqr; + index = i; + } + } + + return index; + } + + public static int getMostSimilar(Vec3f start, Vec3f end, Vec3f... vectors) { + Vec3f.sub(end, start, BASIS_DIRECTION); + float maxDot = Float.MIN_VALUE; + int index = -1; + + for (int i = 0; i < vectors.length; i++) { + if (vectors[i] == null) { + continue; + } + + if (!vectors[i].validateValues()) { + continue; + } + + Vec3f.sub(vectors[i], start, COMPARISION); + float dot = Vec3f.dot(BASIS_DIRECTION, COMPARISION) / BASIS_DIRECTION.length() * COMPARISION.length(); + + if (dot > maxDot) { + maxDot = dot; + index = i; + } + } + + return index; + } + + private static final Vec3f BASIS_DIRECTION = new Vec3f(); + private static final Vec3f COMPARISION = new Vec3f(); + + public static int getMostSimilar(Vec3f start, Vec3f end, List vectors) { + Vec3f.sub(end, start, BASIS_DIRECTION); + float maxDot = Float.MIN_VALUE; + int index = -1; + + for (int i = 0; i < vectors.size(); i++) { + if (vectors.get(i) == null) { + continue; + } + + if (!vectors.get(i).validateValues()) { + continue; + } + + Vec3f.sub(vectors.get(i), start, COMPARISION); + float dot = Vec3f.dot(BASIS_DIRECTION, COMPARISION) / BASIS_DIRECTION.length() * COMPARISION.length(); + + if (dot > maxDot) { + maxDot = dot; + index = i; + } + } + + return index; + } + + public Vector3f toMojangVector() { + return new Vector3f(this.x, this.y, this.z); + } + + public Vec3 toDoubleVector() { + return new Vec3(this.x, this.y, this.z); + } + + public static Vec3f fromMojangVector(Vector3f vec3) { + return new Vec3f(vec3.x(), vec3.y(), vec3.z()); + } + + public static Vec3f fromDoubleVector(Vec3 vec3) { + return new Vec3f((float)vec3.x(), (float)vec3.y(), (float)vec3.z()); + } + + private static final OpenMatrix4f DEST = new OpenMatrix4f(); + + public Vec3f rotateDegree(Vec3f axis, float degree) { + OpenMatrix4f.ofRotationDegree(degree, axis, DEST); + OpenMatrix4f.transform3v(DEST, this, this); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/math/Vec4f.java b/src/main/java/com/tiedup/remake/rig/math/Vec4f.java new file mode 100644 index 0000000..1fd42dc --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/math/Vec4f.java @@ -0,0 +1,75 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.math; + +public class Vec4f extends Vec3f { + public float w; + + public Vec4f() { + super(); + this.w = 0; + } + + public Vec4f(float x, float y, float z, float w) { + super(x, y, z); + this.w = w; + } + + public Vec4f(Vec3f vec3f) { + super(vec3f.x, vec3f.y, vec3f.z); + this.w = 1.0F; + } + + public void set(float x, float y, float z, float w) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + public void set(Vec4f vec4f) { + super.set(vec4f); + this.w = vec4f.w; + } + + public Vec4f add(float x, float y, float z, float w) { + this.x += x; + this.y += y; + this.z += z; + this.w += w; + return this; + } + + public static Vec4f add(Vec4f left, Vec4f right, Vec4f dest) { + if (dest == null) { + dest = new Vec4f(); + } + + dest.x = left.x + right.x; + dest.y = left.y + right.y; + dest.z = left.z + right.z; + dest.w = left.w + right.w; + + return dest; + } + + @Override + public Vec4f scale(float f) { + super.scale(f); + this.w *= f; + return this; + } + + public Vec4f transform(OpenMatrix4f matrix) { + return OpenMatrix4f.transform(matrix, this, this); + } + + @Override + public String toString() { + return "Vec4f[" + this.x + ", " + this.y + ", " + this.z + ", " + this.w + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/ClassicMesh.java b/src/main/java/com/tiedup/remake/rig/mesh/ClassicMesh.java new file mode 100644 index 0000000..27e1e81 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/ClassicMesh.java @@ -0,0 +1,104 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector4f; + +import com.google.common.collect.Maps; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; + +import com.tiedup.remake.rig.mesh.ClassicMesh.ClassicMeshPart; +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import yesman.epicfight.main.EpicFightMod; + +public class ClassicMesh extends StaticMesh { + public ClassicMesh(Map arrayMap, Map> partBuilders, ClassicMesh parent, RenderProperties properties) { + super(arrayMap, partBuilders, parent, properties); + } + + @Override + protected Map createModelPart(Map> partBuilders) { + Map parts = Maps.newHashMap(); + + partBuilders.forEach((partDefinition, vertexBuilder) -> { + parts.put(partDefinition.partName(), new ClassicMeshPart(vertexBuilder, partDefinition.renderProperties(), partDefinition.getModelPartAnimationProvider())); + }); + + return parts; + } + + @Override + protected ClassicMeshPart getOrLogException(Map parts, String name) { + if (!parts.containsKey(name)) { + EpicFightMod.LOGGER.debug("Can not find the mesh part named " + name + " in " + this.getClass().getCanonicalName()); + return null; + } + + return parts.get(name); + } + + @Override + public void draw(PoseStack poseStack, VertexConsumer vertexConsumer, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) { + for (ClassicMeshPart part : this.parts.values()) { + part.draw(poseStack, vertexConsumer, drawingFunction, packedLight, r, g, b, a, overlay); + } + } + + @Override + public void drawPosed(PoseStack poseStack, VertexConsumer vertexConsumer, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, Armature armature, OpenMatrix4f[] poses) { + this.draw(poseStack, vertexConsumer, drawingFunction, packedLight, r, g, b, a, overlay); + } + + public class ClassicMeshPart extends MeshPart { + public ClassicMeshPart(List verticies, @Nullable Mesh.RenderProperties renderProperties, @Nullable Supplier vanillaPartTracer) { + super(verticies, renderProperties, vanillaPartTracer); + } + + protected static final Vector4f POSITION = new Vector4f(); + protected static final Vector3f NORMAL = new Vector3f(); + + @Override + public void draw(PoseStack poseStack, VertexConsumer bufferbuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) { + if (this.isHidden()) { + return; + } + + Vector4f color = this.getColor(r, g, b, a); + poseStack.pushPose(); + OpenMatrix4f transform = this.getVanillaPartTransform(); + + if (transform != null) { + poseStack.mulPoseMatrix(OpenMatrix4f.exportToMojangMatrix(transform)); + } + + Matrix4f matrix4f = poseStack.last().pose(); + Matrix3f matrix3f = poseStack.last().normal(); + + for (VertexBuilder vi : this.getVertices()) { + getVertexPosition(vi.position, POSITION); + getVertexNormal(vi.normal, NORMAL); + POSITION.mul(matrix4f); + NORMAL.mul(matrix3f); + + drawingFunction.draw(bufferbuilder, POSITION.x(), POSITION.y(), POSITION.z(), NORMAL.x(), NORMAL.y(), NORMAL.z(), packedLight, color.x, color.y, color.z, color.w, uvs[vi.uv * 2], uvs[vi.uv * 2 + 1], overlay); + } + + poseStack.popPose(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/CompositeMesh.java b/src/main/java/com/tiedup/remake/rig/mesh/CompositeMesh.java new file mode 100644 index 0000000..f747ee3 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/CompositeMesh.java @@ -0,0 +1,77 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.Map; + +import javax.annotation.Nullable; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; + +import com.tiedup.remake.rig.cloth.ClothSimulatable; +import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject; +import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObjectBuilder; +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.math.OpenMatrix4f; + +public class CompositeMesh implements Mesh, SoftBodyTranslatable { + private final StaticMesh staticMesh; + private final SoftBodyTranslatable softBodyMesh; + + public CompositeMesh(StaticMesh staticMesh, SoftBodyTranslatable softBodyMesh) { + this.staticMesh = staticMesh; + this.softBodyMesh = softBodyMesh; + } + + @Override + public void initialize() { + this.staticMesh.initialize(); + } + + @Override + public void draw(PoseStack poseStack, VertexConsumer bufferBuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) { + this.staticMesh.draw(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay); + this.softBodyMesh.getOriginalMesh().draw(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay); + } + + @Override + public void drawPosed(PoseStack poseStack, VertexConsumer bufferBuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, Armature armature, OpenMatrix4f[] poses) { + this.staticMesh.drawPosed(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay, armature, poses); + this.softBodyMesh.getOriginalMesh().drawPosed(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay, armature, poses); + } + + @Override + public boolean canStartSoftBodySimulation() { + return this.softBodyMesh.canStartSoftBodySimulation(); + } + + @Override + public ClothObject createSimulationData(@Nullable SoftBodyTranslatable provider, ClothSimulatable simOwner, ClothObjectBuilder simBuilder) { + return this.softBodyMesh.createSimulationData(this, simOwner, simBuilder); + } + + @Nullable + public StaticMesh getStaticMesh() { + return this.staticMesh; + } + + @Override + public StaticMesh getOriginalMesh() { + return (StaticMesh)this.softBodyMesh; + } + + @Override + public void putSoftBodySimulationInfo(Map sofyBodySimulationInfo) { + this.softBodyMesh.putSoftBodySimulationInfo(sofyBodySimulationInfo); + } + + @Override + public Map getSoftBodySimulationInfo() { + return this.softBodyMesh.getSoftBodySimulationInfo(); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/HumanoidMesh.java b/src/main/java/com/tiedup/remake/rig/mesh/HumanoidMesh.java new file mode 100644 index 0000000..95270f1 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/HumanoidMesh.java @@ -0,0 +1,65 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.List; +import java.util.Map; + +import net.minecraft.world.entity.EquipmentSlot; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.mesh.MeshPartDefinition; +import com.tiedup.remake.rig.mesh.Meshes; +import com.tiedup.remake.rig.mesh.SkinnedMesh; +import com.tiedup.remake.rig.mesh.VertexBuilder; + +public class HumanoidMesh extends SkinnedMesh { + public final SkinnedMeshPart head; + public final SkinnedMeshPart torso; + public final SkinnedMeshPart leftArm; + public final SkinnedMeshPart rightArm; + public final SkinnedMeshPart leftLeg; + public final SkinnedMeshPart rightLeg; + public final SkinnedMeshPart hat; + public final SkinnedMeshPart jacket; + public final SkinnedMeshPart leftSleeve; + public final SkinnedMeshPart rightSleeve; + public final SkinnedMeshPart leftPants; + public final SkinnedMeshPart rightPants; + + public HumanoidMesh(Map arrayMap, Map> parts, SkinnedMesh parent, RenderProperties properties) { + super(arrayMap, parts, parent, properties); + + this.head = this.getOrLogException(this.parts, "head"); + this.torso = this.getOrLogException(this.parts, "torso"); + this.leftArm = this.getOrLogException(this.parts, "leftArm"); + this.rightArm = this.getOrLogException(this.parts, "rightArm"); + this.leftLeg = this.getOrLogException(this.parts, "leftLeg"); + this.rightLeg = this.getOrLogException(this.parts, "rightLeg"); + + this.hat = this.getOrLogException(this.parts, "hat"); + this.jacket = this.getOrLogException(this.parts, "jacket"); + this.leftSleeve = this.getOrLogException(this.parts, "leftSleeve"); + this.rightSleeve = this.getOrLogException(this.parts, "rightSleeve"); + this.leftPants = this.getOrLogException(this.parts, "leftPants"); + this.rightPants = this.getOrLogException(this.parts, "rightPants"); + } + + public AssetAccessor getHumanoidArmorModel(EquipmentSlot slot) { + switch (slot) { + case HEAD: + return Meshes.HELMET; + case CHEST: + return Meshes.CHESTPLATE; + case LEGS: + return Meshes.LEGGINS; + case FEET: + return Meshes.BOOTS; + default: + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/Mesh.java b/src/main/java/com/tiedup/remake/rig/mesh/Mesh.java new file mode 100644 index 0000000..6b7f143 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/Mesh.java @@ -0,0 +1,214 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import javax.annotation.Nullable; + +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector4f; +import org.lwjgl.system.MemoryStack; + +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; + +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.core.Vec3i; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.client.extensions.IForgeVertexConsumer; +import net.minecraftforge.client.model.IQuadTransformer; +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; +import com.tiedup.remake.rig.render.TiedUpRenderTypes; + +public interface Mesh { + + void initialize(); + + /* Draw wihtout mesh deformation */ + void draw(PoseStack poseStack, VertexConsumer vertexConsumer, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay); + + /* Draw with mesh deformation */ + void drawPosed(PoseStack poseStack, VertexConsumer vertexConsumer, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses); + + /* Universal method */ + default void draw(PoseStack poseStack, MultiBufferSource bufferSources, RenderType renderType, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses) { + this.drawPosed(poseStack, bufferSources.getBuffer(EpicFightRenderTypes.getTriangulated(renderType)), drawingFunction, packedLight, r, g, b, a, overlay, armature, poses); + } + + public static record RenderProperties(ResourceLocation customTexturePath, Vec3f customColor, boolean isTransparent) { + public static class Builder { + protected String customTexturePath; + protected Vec3f customColor = new Vec3f(); + protected boolean isTransparent; + + public RenderProperties.Builder customTexturePath(String path) { + this.customTexturePath = path; + return this; + } + + public RenderProperties.Builder transparency(boolean isTransparent) { + this.isTransparent = isTransparent; + return this; + } + + public RenderProperties.Builder customColor(float r, float g, float b) { + this.customColor.x = r; + this.customColor.y = g; + this.customColor.z = b; + return this; + } + + public RenderProperties build() { + return new RenderProperties(this.customTexturePath == null ? null : ResourceLocation.parse(this.customTexturePath), this.customColor, this.isTransparent); + } + + public static RenderProperties.Builder create() { + return new RenderProperties.Builder(); + } + } + } + + @FunctionalInterface + public interface DrawingFunction { + public static final DrawingFunction NEW_ENTITY = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> { + builder.vertex(posX, posY, posZ, r, g, b, a, u, v, overlay, packedLight, normX, normY, normZ); + }; + + public static final DrawingFunction POSITION_TEX = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> { + builder.vertex(posX, posY, posZ); + builder.uv(u, v); + builder.endVertex(); + }; + + public static final DrawingFunction POSITION_TEX_COLOR_NORMAL = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> { + builder.vertex(posX, posY, posZ); + builder.uv(u, v); + builder.color(r, g, b, a); + builder.normal(normX, normY, normZ); + builder.endVertex(); + }; + + public static final DrawingFunction POSITION_TEX_COLOR_LIGHTMAP = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> { + builder.vertex(posX, posY, posZ); + builder.uv(u, v); + builder.color(r, g, b, a); + builder.uv2(packedLight); + builder.endVertex(); + }; + + public static final DrawingFunction POSITION_COLOR_LIGHTMAP = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> { + builder.vertex(posX, posY, posZ); + builder.color(r, g, b, a); + builder.uv2(packedLight); + builder.endVertex(); + }; + + public static final DrawingFunction POSITION_COLOR_NORMAL = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> { + builder.vertex(posX, posY, posZ); + builder.color(r, g, b, a); + builder.normal(normX, normY, normZ); + builder.endVertex(); + }; + + public static final DrawingFunction POSITION_COLOR_TEX_LIGHTMAP = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> { + builder.vertex(posX, posY, posZ); + builder.color(r, g, b, a); + builder.uv(u, v); + builder.uv2(packedLight); + builder.endVertex(); + }; + + public void draw(VertexConsumer vertexConsumer, float posX, float posY, float posZ, float normX, float normY, float normZ, int packedLight, float r, float g, float b, float a, float u, float v, int overlay); + + default void putBulkData(PoseStack.Pose pose, BakedQuad bakedQuad, VertexConsumer vertexConsumer, float red, float green, float blue, float alpha, int packedLight, int packedOverlay, boolean readExistingColor) { + putBulkDataWithDrawingFunction(this, vertexConsumer, pose, bakedQuad, new float[] { 1.0F, 1.0F, 1.0F, 1.0F }, red, green, blue, alpha, new int[] { packedLight, packedLight, packedLight, packedLight }, packedOverlay, readExistingColor); + } + + static void putBulkDataWithDrawingFunction(DrawingFunction drawingFunction, VertexConsumer builder, PoseStack.Pose pPoseEntry, BakedQuad pQuad, float[] pColorMuls, float pRed, float pGreen, float pBlue, float alpha, int[] pCombinedLights, int pCombinedOverlay, boolean pMulColor) { + float[] afloat = new float[] { pColorMuls[0], pColorMuls[1], pColorMuls[2], pColorMuls[3] }; + int[] aint1 = pQuad.getVertices(); + Vec3i vec3i = pQuad.getDirection().getNormal(); + Matrix4f matrix4f = pPoseEntry.pose(); + Vector3f vector3f = pPoseEntry.normal().transform(new Vector3f((float) vec3i.getX(), (float) vec3i.getY(), (float) vec3i.getZ())); + int j = aint1.length / 8; + + try (MemoryStack memorystack = MemoryStack.stackPush()) { + ByteBuffer bytebuffer = memorystack.malloc(DefaultVertexFormat.BLOCK.getVertexSize()); + IntBuffer intbuffer = bytebuffer.asIntBuffer(); + + for (int k = 0; k < j; ++k) { + intbuffer.clear(); + intbuffer.put(aint1, k * 8, 8); + float f = bytebuffer.getFloat(0); + float f1 = bytebuffer.getFloat(4); + float f2 = bytebuffer.getFloat(8); + float f3; + float f4; + float f5; + + if (pMulColor) { + float f6 = (float) (bytebuffer.get(12) & 255) / 255.0F; + float f7 = (float) (bytebuffer.get(13) & 255) / 255.0F; + float f8 = (float) (bytebuffer.get(14) & 255) / 255.0F; + f3 = f6 * afloat[k] * pRed; + f4 = f7 * afloat[k] * pGreen; + f5 = f8 * afloat[k] * pBlue; + } else { + f3 = afloat[k] * pRed; + f4 = afloat[k] * pGreen; + f5 = afloat[k] * pBlue; + } + + int l = applyBakedLighting(pCombinedLights[k], bytebuffer); + float f9 = bytebuffer.getFloat(16); + float f10 = bytebuffer.getFloat(20); + Vector4f vector4f = matrix4f.transform(new Vector4f(f, f1, f2, 1.0F)); + applyBakedNormals(vector3f, bytebuffer, pPoseEntry.normal()); + float vertexAlpha = pMulColor ? alpha * (float) (bytebuffer.get(15) & 255) / 255.0F : alpha; + drawingFunction.draw(builder, vector4f.x(), vector4f.y(), vector4f.z(), vector3f.x(), vector3f.y(), vector3f.z(), l, f3, f4, f5, vertexAlpha, f9, f10, pCombinedOverlay); + } + } + } + + /** + * Code copy from {@link IForgeVertexConsumer#applyBakedLighting} + */ + static int applyBakedLighting(int packedLight, ByteBuffer data) { + int bl = packedLight & 0xFFFF; + int sl = (packedLight >> 16) & 0xFFFF; + int offset = IQuadTransformer.UV2 * 4; // int offset for vertex 0 * 4 bytes per int + int blBaked = Short.toUnsignedInt(data.getShort(offset)); + int slBaked = Short.toUnsignedInt(data.getShort(offset + 2)); + bl = Math.max(bl, blBaked); + sl = Math.max(sl, slBaked); + return bl | (sl << 16); + } + + /** + * Code copy from {@link IForgeVertexConsumer#applyBakedNormals} + */ + static void applyBakedNormals(Vector3f generated, ByteBuffer data, Matrix3f normalTransform) { + byte nx = data.get(28); + byte ny = data.get(29); + byte nz = data.get(30); + if (nx != 0 || ny != 0 || nz != 0) + { + generated.set(nx / 127f, ny / 127f, nz / 127f); + generated.mul(normalTransform); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/MeshPart.java b/src/main/java/com/tiedup/remake/rig/mesh/MeshPart.java new file mode 100644 index 0000000..5373af6 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/MeshPart.java @@ -0,0 +1,89 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.List; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import org.joml.Vector4f; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; + +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.render.TiedUpRenderTypes; + +public abstract class MeshPart { + protected final List verticies; + protected final Mesh.RenderProperties renderProperties; + protected final Supplier vanillaPartTracer; + protected boolean isHidden; + + public MeshPart(List vertices, @Nullable Mesh.RenderProperties renderProperties, @Nullable Supplier vanillaPartTracer) { + this.verticies = vertices; + this.renderProperties = renderProperties; + this.vanillaPartTracer = vanillaPartTracer; + } + + public abstract void draw(PoseStack poseStack, VertexConsumer bufferbuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay); + + public void setHidden(boolean hidden) { + this.isHidden = hidden; + } + + public boolean isHidden() { + return this.isHidden; + } + + public List getVertices() { + return this.verticies; + } + + public OpenMatrix4f getVanillaPartTransform() { + if (this.vanillaPartTracer == null) { + return null; + } + + return this.vanillaPartTracer.get(); + } + + public VertexConsumer getBufferBuilder(RenderType renderType, MultiBufferSource bufferSource) { + if (this.renderProperties.customTexturePath() != null) { + return bufferSource.getBuffer(EpicFightRenderTypes.replaceTexture(this.renderProperties.customTexturePath(), renderType)); + } + + return bufferSource.getBuffer(renderType); + } + + protected static final Vector4f COLOR = new Vector4f(); + + public Vector4f getColor(float r, float g, float b, float a) { + if (this.renderProperties != null && this.renderProperties.customColor() != null) { + COLOR.set( + this.renderProperties.customColor().x + , this.renderProperties.customColor().y + , this.renderProperties.customColor().z + , a + ); + + return COLOR; + } else { + COLOR.set( + r + , g + , b + , a + ); + + return COLOR; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/MeshPartDefinition.java b/src/main/java/com/tiedup/remake/rig/mesh/MeshPartDefinition.java new file mode 100644 index 0000000..8f3ecbf --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/MeshPartDefinition.java @@ -0,0 +1,17 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.function.Supplier; + +import com.tiedup.remake.rig.math.OpenMatrix4f; + +public interface MeshPartDefinition { + String partName(); + Mesh.RenderProperties renderProperties(); + Supplier getModelPartAnimationProvider(); +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/Meshes.java b/src/main/java/com/tiedup/remake/rig/mesh/Meshes.java new file mode 100644 index 0000000..886f314 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/Meshes.java @@ -0,0 +1,225 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import com.google.common.collect.Maps; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.asset.JsonAssetLoader; +import com.tiedup.remake.rig.mesh.Mesh.RenderProperties; +import com.tiedup.remake.rig.cloth.ClothSimulatable; +import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject; +import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObjectBuilder; +import com.tiedup.remake.rig.mesh.CreeperMesh; +import com.tiedup.remake.rig.mesh.DragonMesh; +import com.tiedup.remake.rig.mesh.EndermanMesh; +import com.tiedup.remake.rig.mesh.HoglinMesh; +import com.tiedup.remake.rig.mesh.HumanoidMesh; +import com.tiedup.remake.rig.mesh.IronGolemMesh; +import com.tiedup.remake.rig.mesh.PiglinMesh; +import com.tiedup.remake.rig.mesh.RavagerMesh; +import com.tiedup.remake.rig.mesh.SpiderMesh; +import com.tiedup.remake.rig.mesh.VexMesh; +import com.tiedup.remake.rig.mesh.VillagerMesh; +import com.tiedup.remake.rig.mesh.WitherMesh; +import yesman.epicfight.main.EpicFightMod; + +public class Meshes implements PreparableReloadListener { + private static final Map> ACCESSORS = Maps.newHashMap(); + private static final Map, Mesh> MESHES = Maps.newHashMap(); + private static ResourceManager resourceManager = null; + + //For resource reloader + public static final Meshes INSTANCE = new Meshes(); + + // Entities + public static final MeshAccessor ALEX = MeshAccessor.create(EpicFightMod.MODID, "entity/biped_slim_arm", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HumanoidMesh::new)); + public static final MeshAccessor BIPED = MeshAccessor.create(EpicFightMod.MODID, "entity/biped", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HumanoidMesh::new)); + public static final MeshAccessor BIPED_OLD_TEX = MeshAccessor.create(EpicFightMod.MODID, "entity/biped_old_texture", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HumanoidMesh::new)); + public static final MeshAccessor BIPED_OUTLAYER = MeshAccessor.create(EpicFightMod.MODID, "entity/biped_outlayer", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HumanoidMesh::new)); + public static final MeshAccessor VILLAGER_ZOMBIE = MeshAccessor.create(EpicFightMod.MODID, "entity/zombie_villager", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(VillagerMesh::new)); + public static final MeshAccessor CREEPER = MeshAccessor.create(EpicFightMod.MODID, "entity/creeper", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(CreeperMesh::new)); + public static final MeshAccessor ENDERMAN = MeshAccessor.create(EpicFightMod.MODID, "entity/enderman", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(EndermanMesh::new)); + public static final MeshAccessor SKELETON = MeshAccessor.create(EpicFightMod.MODID, "entity/skeleton", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HumanoidMesh::new)); + public static final MeshAccessor SPIDER = MeshAccessor.create(EpicFightMod.MODID, "entity/spider", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(SpiderMesh::new)); + public static final MeshAccessor IRON_GOLEM = MeshAccessor.create(EpicFightMod.MODID, "entity/iron_golem", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(IronGolemMesh::new)); + public static final MeshAccessor ILLAGER = MeshAccessor.create(EpicFightMod.MODID, "entity/illager", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(VillagerMesh::new)); + public static final MeshAccessor WITCH = MeshAccessor.create(EpicFightMod.MODID, "entity/witch", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(VillagerMesh::new)); + public static final MeshAccessor RAVAGER = MeshAccessor.create(EpicFightMod.MODID, "entity/ravager",(jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(RavagerMesh::new)); + public static final MeshAccessor VEX = MeshAccessor.create(EpicFightMod.MODID, "entity/vex", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(VexMesh::new)); + public static final MeshAccessor PIGLIN = MeshAccessor.create(EpicFightMod.MODID, "entity/piglin", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(PiglinMesh::new)); + public static final MeshAccessor HOGLIN = MeshAccessor.create(EpicFightMod.MODID, "entity/hoglin", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HoglinMesh::new)); + public static final MeshAccessor DRAGON = MeshAccessor.create(EpicFightMod.MODID, "entity/dragon", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(DragonMesh::new)); + public static final MeshAccessor WITHER = MeshAccessor.create(EpicFightMod.MODID, "entity/wither", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(WitherMesh::new)); + + // Armors + public static final MeshAccessor HELMET = MeshAccessor.create(EpicFightMod.MODID, "armor/helmet", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(SkinnedMesh::new)); + public static final MeshAccessor HELMET_PIGLIN = MeshAccessor.create(EpicFightMod.MODID, "armor/piglin_helmet", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(SkinnedMesh::new)); + public static final MeshAccessor HELMET_VILLAGER = MeshAccessor.create(EpicFightMod.MODID, "armor/villager_helmet", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(SkinnedMesh::new)); + public static final MeshAccessor CHESTPLATE = MeshAccessor.create(EpicFightMod.MODID, "armor/chestplate", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(SkinnedMesh::new)); + public static final MeshAccessor LEGGINS = MeshAccessor.create(EpicFightMod.MODID, "armor/leggins", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(SkinnedMesh::new)); + public static final MeshAccessor BOOTS = MeshAccessor.create(EpicFightMod.MODID, "armor/boots", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(SkinnedMesh::new)); + + // Particles + public static final MeshAccessor AIR_BURST = MeshAccessor.create(EpicFightMod.MODID, "particle/air_burst", (jsonModelLoader) -> jsonModelLoader.loadClassicMesh(ClassicMesh::new)); + public static final MeshAccessor FORCE_FIELD = MeshAccessor.create(EpicFightMod.MODID, "particle/force_field", (jsonModelLoader) -> jsonModelLoader.loadClassicMesh(ClassicMesh::new)); + public static final MeshAccessor LASER = MeshAccessor.create(EpicFightMod.MODID, "particle/laser", (jsonModelLoader) -> jsonModelLoader.loadClassicMesh(ClassicMesh::new)); + + // Layers + public static final MeshAccessor CAPE_DEFAULT = MeshAccessor.create(EpicFightMod.MODID, "layer/default_cape", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(SkinnedMesh::new)); + + public static void reload(ResourceManager resourceManager) { + Meshes.resourceManager = resourceManager; + + ACCESSORS.entrySet().removeIf(entry -> !entry.getValue().inRegistry); + + MESHES.values().forEach((mesh) -> { + if (mesh instanceof SkinnedMesh skinnedMesh) { + skinnedMesh.destroy(); + } + }); + + MESHES.clear(); + } + + @SuppressWarnings("unchecked") + @Nullable + public static AssetAccessor get(ResourceLocation id) { + return (AssetAccessor) ACCESSORS.get(id); + } + + @SuppressWarnings("unchecked") + public static AssetAccessor getOrCreate(ResourceLocation id, Function jsonLoader) { + return ACCESSORS.containsKey(id) ? (AssetAccessor)ACCESSORS.get(id) : MeshAccessor.create(id, jsonLoader, false); + } + + @SuppressWarnings("unchecked") + public static Set> entry(Class filter) { + return ACCESSORS.values().stream().filter((accessor) -> filter.isAssignableFrom(accessor.get().getClass())).map((accessor) -> (AssetAccessor)accessor).collect(Collectors.toSet()); + } + + public static ResourceLocation wrapLocation(ResourceLocation rl) { + return rl.getPath().matches("animmodels/.*\\.json") ? rl : ResourceLocation.fromNamespaceAndPath(rl.getNamespace(), "animmodels/" + rl.getPath() + ".json"); + } + + @Override + public CompletableFuture reload(PreparableReloadListener.PreparationBarrier stage, ResourceManager resourceManager, ProfilerFiller preparationsProfiler, ProfilerFiller reloadProfiler, Executor backgroundExecutor, Executor gameExecutor) { + return CompletableFuture.runAsync(() -> { + Meshes.reload(resourceManager); + }, gameExecutor).thenCompose(stage::wait); + } + + @FunctionalInterface + public interface MeshContructor

> { + M invoke(Map arrayMap, Map> parts, M parent, RenderProperties properties); + } + + public static record MeshAccessor (ResourceLocation registryName, Function jsonLoader, boolean inRegistry) implements AssetAccessor, SoftBodyTranslatable { + public static MeshAccessor create(String namespaceId, String path, Function jsonLoader) { + return create(ResourceLocation.fromNamespaceAndPath(namespaceId, path), jsonLoader, true); + } + + private static MeshAccessor create(ResourceLocation id, Function jsonLoader, boolean inRegistry) { + MeshAccessor accessor = new MeshAccessor (id, jsonLoader, inRegistry); + ACCESSORS.put(id, accessor); + return accessor; + } + + @SuppressWarnings("unchecked") + @Override + public M get() { + if (!MESHES.containsKey(this)) { + JsonAssetLoader jsonModelLoader = new JsonAssetLoader(resourceManager, wrapLocation(this.registryName)); + MESHES.put(this, this.jsonLoader.apply(jsonModelLoader)); + } + + return (M)MESHES.get(this); + } + + public String toString() { + return this.registryName.toString(); + } + + public int hashCode() { + return this.registryName.hashCode(); + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof MeshAccessor armatureAccessor) { + return this.registryName.equals(armatureAccessor.registryName()); + } else if (obj instanceof ResourceLocation rl) { + return this.registryName.equals(rl); + } else if (obj instanceof String name) { + return this.registryName.toString().equals(name); + } else { + return false; + } + } + + @Override + public boolean canStartSoftBodySimulation() { + Mesh mesh = this.get(); + + if (mesh instanceof StaticMesh staticMesh) { + return staticMesh.canStartSoftBodySimulation(); + } else if (mesh instanceof CompositeMesh compositeMesh) { + return compositeMesh.canStartSoftBodySimulation(); + } + + return false; + } + + @Override + public ClothObject createSimulationData(SoftBodyTranslatable provider, ClothSimulatable simOwner, ClothObjectBuilder simBuilder) { + Mesh mesh = this.get(); + + if (mesh instanceof StaticMesh staticMesh) { + return staticMesh.createSimulationData(provider, simOwner, simBuilder); + } else if (mesh instanceof CompositeMesh compositeMesh) { + return compositeMesh.createSimulationData(provider, simOwner, simBuilder); + } + + return null; + } + + @Override + public void putSoftBodySimulationInfo(Map sofyBodySimulationInfo) { + Mesh mesh = this.get(); + + if (mesh instanceof SoftBodyTranslatable softBodyTranslatable) { + softBodyTranslatable.putSoftBodySimulationInfo(sofyBodySimulationInfo); + } + } + + @Override + public Map getSoftBodySimulationInfo() { + Mesh mesh = this.get(); + + if (mesh instanceof SoftBodyTranslatable softBodyTranslatable) { + return softBodyTranslatable.getSoftBodySimulationInfo(); + } else { + return null; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/SingleGroupVertexBuilder.java b/src/main/java/com/tiedup/remake/rig/mesh/SingleGroupVertexBuilder.java new file mode 100644 index 0000000..b964209 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/SingleGroupVertexBuilder.java @@ -0,0 +1,157 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.List; +import java.util.Map; + +import com.google.common.collect.Maps; + +import it.unimi.dsi.fastutil.floats.FloatArrayList; +import it.unimi.dsi.fastutil.floats.FloatList; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import com.tiedup.remake.rig.math.Vec2f; +import com.tiedup.remake.rig.math.Vec3f; + +public class SingleGroupVertexBuilder { + private Vec3f position; + private Vec3f normal; + private Vec2f textureCoordinate; + private Vec3f effectiveJointIDs; + private Vec3f effectiveJointWeights; + private int effectiveJointNumber; + + public SingleGroupVertexBuilder() { + this.position = null; + this.normal = null; + this.textureCoordinate = null; + } + + public SingleGroupVertexBuilder(SingleGroupVertexBuilder vertex) { + this.position = vertex.position; + this.effectiveJointIDs = vertex.effectiveJointIDs; + this.effectiveJointWeights = vertex.effectiveJointWeights; + this.effectiveJointNumber = vertex.effectiveJointNumber; + } + + public SingleGroupVertexBuilder setPosition(Vec3f position) { + this.position = position; + return this; + } + + public SingleGroupVertexBuilder setNormal(Vec3f vector) { + this.normal = vector; + return this; + } + + public SingleGroupVertexBuilder setTextureCoordinate(Vec2f vector) { + this.textureCoordinate = vector; + return this; + } + + public SingleGroupVertexBuilder setEffectiveJointIDs(Vec3f effectiveJointIDs) { + this.effectiveJointIDs = effectiveJointIDs; + return this; + } + + public SingleGroupVertexBuilder setEffectiveJointWeights(Vec3f effectiveJointWeights) { + this.effectiveJointWeights = effectiveJointWeights; + return this; + } + + public SingleGroupVertexBuilder setEffectiveJointNumber(int count) { + this.effectiveJointNumber = count; + return this; + } + + public State compareTextureCoordinateAndNormal(Vec3f normal, Vec2f textureCoord) { + if (this.textureCoordinate == null) { + return State.EMPTY; + } else if (this.textureCoordinate.equals(textureCoord) && this.normal.equals(normal)) { + return State.EQUAL; + } else { + return State.DIFFERENT; + } + } + + public static SkinnedMesh loadVertexInformation(List vertices, Map indices) { + FloatList positions = new FloatArrayList(); + FloatList normals = new FloatArrayList(); + FloatList texCoords = new FloatArrayList(); + IntList animationIndices = new IntArrayList(); + FloatList jointWeights = new FloatArrayList(); + IntList affectCountList = new IntArrayList(); + + for (int i = 0; i < vertices.size(); i++) { + SingleGroupVertexBuilder vertex = vertices.get(i); + Vec3f position = vertex.position; + Vec3f normal = vertex.normal; + Vec2f texCoord = vertex.textureCoordinate; + positions.add(position.x); + positions.add(position.y); + positions.add(position.z); + normals.add(normal.x); + normals.add(normal.y); + normals.add(normal.z); + texCoords.add(texCoord.x); + texCoords.add(texCoord.y); + + Vec3f effectIDs = vertex.effectiveJointIDs; + Vec3f weights = vertex.effectiveJointWeights; + int count = Math.min(vertex.effectiveJointNumber, 3); + affectCountList.add(count); + + for (int j = 0; j < count; j++) { + switch (j) { + case 0: + animationIndices.add((int) effectIDs.x); + jointWeights.add(weights.x); + animationIndices.add(jointWeights.size() - 1); + break; + case 1: + animationIndices.add((int) effectIDs.y); + jointWeights.add(weights.y); + animationIndices.add(jointWeights.size() - 1); + break; + case 2: + animationIndices.add((int) effectIDs.z); + jointWeights.add(weights.z); + animationIndices.add(jointWeights.size() - 1); + break; + default: + } + } + } + + Float[] positionList = positions.toArray(new Float[0]); + Float[] normalList = normals.toArray(new Float[0]); + Float[] texCoordList = texCoords.toArray(new Float[0]); + Integer[] affectingJointIndices = animationIndices.toArray(new Integer[0]); + Float[] jointWeightList = jointWeights.toArray(new Float[0]); + Integer[] affectJointCounts = affectCountList.toArray(new Integer[0]); + Map arrayMap = Maps.newHashMap(); + Map> meshDefinitions = Maps.newHashMap(); + + arrayMap.put("positions", positionList); + arrayMap.put("normals", normalList); + arrayMap.put("uvs", texCoordList); + arrayMap.put("weights", jointWeightList); + arrayMap.put("vcounts", affectJointCounts); + arrayMap.put("vindices", affectingJointIndices); + + for (Map.Entry e : indices.entrySet()) { + meshDefinitions.put(e.getKey(), VertexBuilder.create(e.getValue().toIntArray())); + } + + return new SkinnedMesh(arrayMap, meshDefinitions, null, null); + } + + public enum State { + EMPTY, EQUAL, DIFFERENT + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/SkinnedMesh.java b/src/main/java/com/tiedup/remake/rig/mesh/SkinnedMesh.java new file mode 100644 index 0000000..6de8ef4 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/SkinnedMesh.java @@ -0,0 +1,398 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector4f; + +import com.google.common.collect.Maps; +import com.google.gson.JsonObject; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import com.tiedup.remake.rig.asset.JsonAssetLoader; +import com.tiedup.remake.rig.mesh.SkinnedMesh.SkinnedMeshPart; +import com.tiedup.remake.rig.armature.Armature; +import com.tiedup.remake.rig.util.ParseUtil; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec4f; +import com.tiedup.remake.rig.render.TiedUpRenderTypes; +import yesman.epicfight.client.renderer.shader.compute.ComputeShaderSetup; +import yesman.epicfight.client.renderer.shader.compute.loader.ComputeShaderProvider; +import yesman.epicfight.config.ClientConfig; +import yesman.epicfight.main.EpicFightMod; +import yesman.epicfight.main.EpicFightSharedConstants; + +public class SkinnedMesh extends StaticMesh { + protected final float[] weights; + protected final int[] affectingJointCounts; + protected final int[][] affectingWeightIndices; + protected final int[][] affectingJointIndices; + + private final int maxJointCount; + + @Nullable + private ComputeShaderSetup computerShaderSetup; + + public SkinnedMesh(@Nullable Map arrayMap, @Nullable Map> partBuilders, @Nullable SkinnedMesh parent, RenderProperties properties) { + super(arrayMap, partBuilders, parent, properties); + + this.weights = parent == null ? ParseUtil.unwrapFloatWrapperArray(arrayMap.get("weights")) : parent.weights; + this.affectingJointCounts = parent == null ? ParseUtil.unwrapIntWrapperArray(arrayMap.get("vcounts")) : parent.affectingJointCounts; + + if (parent != null) { + this.affectingJointIndices = parent.affectingJointIndices; + this.affectingWeightIndices = parent.affectingWeightIndices; + } else { + int[] vindices = ParseUtil.unwrapIntWrapperArray(arrayMap.get("vindices")); + this.affectingJointIndices = new int[this.affectingJointCounts.length][]; + this.affectingWeightIndices = new int[this.affectingJointCounts.length][]; + int idx = 0; + + for (int i = 0; i < this.affectingJointCounts.length; i++) { + int count = this.affectingJointCounts[i]; + int[] jointId = new int[count]; + int[] weights = new int[count]; + + for (int j = 0; j < count; j++) { + jointId[j] = vindices[idx * 2]; + weights[j] = vindices[idx * 2 + 1]; + idx++; + } + + this.affectingJointIndices[i] = jointId; + this.affectingWeightIndices[i] = weights; + } + } + + int maxJointId = 0; + + for (int[] i : this.affectingJointIndices) { + for (int j : i) { + if (maxJointId < j) { + maxJointId = j; + } + } + } + + this.maxJointCount = maxJointId; + + if (ComputeShaderProvider.supportComputeShader()) { + if (RenderSystem.isOnRenderThread()) { + this.computerShaderSetup = ComputeShaderProvider.getComputeShaderSetup(this); + } else { + RenderSystem.recordRenderCall(() -> { + this.computerShaderSetup = ComputeShaderProvider.getComputeShaderSetup(this); + }); + } + } + } + + public void destroy() { + if (RenderSystem.isOnRenderThread()) { + if (this.computerShaderSetup != null) { + this.computerShaderSetup.destroyBuffers(); + } + } else { + RenderSystem.recordRenderCall(() -> { + if (this.computerShaderSetup != null) { + this.computerShaderSetup.destroyBuffers(); + } + }); + } + } + + @Override + protected Map createModelPart(Map> partBuilders) { + Map parts = Maps.newHashMap(); + + partBuilders.forEach((partDefinition, vertexBuilder) -> { + parts.put(partDefinition.partName(), new SkinnedMeshPart(vertexBuilder, partDefinition.renderProperties(), partDefinition.getModelPartAnimationProvider())); + }); + + return parts; + } + + @Override + protected SkinnedMeshPart getOrLogException(Map parts, String name) { + if (!parts.containsKey(name)) { + if (EpicFightSharedConstants.IS_DEV_ENV) { + EpicFightMod.LOGGER.debug("Cannot find the mesh part named " + name + " in " + this.getClass().getCanonicalName()); + } + + return null; + } + + return parts.get(name); + } + + private static final Vec4f TRANSFORM = new Vec4f(); + private static final Vec4f POS = new Vec4f(); + private static final Vec4f TOTAL_POS = new Vec4f(); + + @Override + public void getVertexPosition(int positionIndex, Vector4f dest, @Nullable OpenMatrix4f[] poses) { + int index = positionIndex * 3; + + POS.set(this.positions[index], this.positions[index + 1], this.positions[index + 2], 1.0F); + TOTAL_POS.set(0.0F, 0.0F, 0.0F, 0.0F); + + for (int i = 0; i < this.affectingJointCounts[positionIndex]; i++) { + int jointIndex = this.affectingJointIndices[positionIndex][i]; + int weightIndex = this.affectingWeightIndices[positionIndex][i]; + float weight = this.weights[weightIndex]; + + Vec4f.add(OpenMatrix4f.transform(poses[jointIndex], POS, TRANSFORM).scale(weight), TOTAL_POS, TOTAL_POS); + } + + dest.set(TOTAL_POS.x, TOTAL_POS.y, TOTAL_POS.z, 1.0F); + } + + private static final Vec4f NORM = new Vec4f(); + private static final Vec4f TOTAL_NORM = new Vec4f(); + + @Override + public void getVertexNormal(int positionIndex, int normalIndex, Vector3f dest, @Nullable OpenMatrix4f[] poses) { + int index = normalIndex * 3; + NORM.set(this.normals[index], this.normals[index + 1], this.normals[index + 2], 1.0F); + TOTAL_NORM.set(0.0F, 0.0F, 0.0F, 0.0F); + + for (int i = 0; i < this.affectingJointCounts[positionIndex]; i++) { + int jointIndex = this.affectingJointIndices[positionIndex][i]; + int weightIndex = this.affectingWeightIndices[positionIndex][i]; + float weight = this.weights[weightIndex]; + Vec4f.add(OpenMatrix4f.transform(poses[jointIndex], NORM, TRANSFORM).scale(weight), TOTAL_NORM, TOTAL_NORM); + } + + dest.set(TOTAL_NORM.x, TOTAL_NORM.y, TOTAL_NORM.z); + } + + /** + * Draws the model without applying animation + */ + @Override + public void draw(PoseStack poseStack, VertexConsumer bufferbuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) { + for (SkinnedMeshPart part : this.parts.values()) { + part.draw(poseStack, bufferbuilder, drawingFunction, packedLight, r, g, b, a, overlay); + } + } + + protected static final Vector4f POSITION = new Vector4f(); + protected static final Vector3f NORMAL = new Vector3f(); + + /** + * Draws the model to vanilla buffer + */ + @Override + public void drawPosed(PoseStack poseStack, VertexConsumer bufferbuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses) { + Matrix4f pose = poseStack.last().pose(); + Matrix3f normal = poseStack.last().normal(); + + for (SkinnedMeshPart part : this.parts.values()) { + if (!part.isHidden()) { + OpenMatrix4f transform = part.getVanillaPartTransform(); + + for (int i = 0; i < poses.length; i++) { + ComputeShaderSetup.TOTAL_POSES[i].load(poses[i]); + + if (armature != null) { + ComputeShaderSetup.TOTAL_POSES[i].mulBack(armature.searchJointById(i).getToOrigin()); + } + + if (transform != null) { + ComputeShaderSetup.TOTAL_POSES[i].mulBack(transform); + } + + ComputeShaderSetup.TOTAL_NORMALS[i] = ComputeShaderSetup.TOTAL_POSES[i].removeTranslation(); + } + + for (VertexBuilder vi : part.getVertices()) { + this.getVertexPosition(vi.position, POSITION, ComputeShaderSetup.TOTAL_POSES); + this.getVertexNormal(vi.position, vi.normal, NORMAL, ComputeShaderSetup.TOTAL_NORMALS); + + POSITION.mul(pose); + NORMAL.mul(normal); + + drawingFunction.draw(bufferbuilder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x, NORMAL.y, NORMAL.z, packedLight, r, g, b, a, this.uvs[vi.uv * 2], this.uvs[vi.uv * 2 + 1], overlay); + } + } + } + } + + /** + * Draws the model depending on animation shader option + * @param armature give this parameter as null if @param poses already bound origin translation + * @param poses + */ + public void draw(PoseStack poseStack, MultiBufferSource bufferSources, RenderType renderType, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses) { + this.draw(poseStack, bufferSources, renderType, Mesh.DrawingFunction.NEW_ENTITY, packedLight, r, g, b, a, overlay, armature, poses); + } + + @Override + public void draw(PoseStack poseStack, MultiBufferSource bufferSources, RenderType renderType, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses) { + if (ClientConfig.activateComputeShader && this.computerShaderSetup != null) { + this.computerShaderSetup.drawWithShader(this, poseStack, bufferSources, EpicFightRenderTypes.getTriangulated(renderType), packedLight, r, g, b, a, overlay, armature, poses); + } else { + this.drawPosed(poseStack, bufferSources.getBuffer(EpicFightRenderTypes.getTriangulated(renderType)), drawingFunction, packedLight, r, g, b, a, overlay, armature, poses); + } + } + + public int getMaxJointCount() { + return this.maxJointCount; + } + + public float[] weights() { + return this.weights; + } + + public int[] affectingJointCounts() { + return this.affectingJointCounts; + } + + public int[][] affectingWeightIndices() { + return this.affectingWeightIndices; + } + + public int[][] affectingJointIndices() { + return this.affectingJointIndices; + } + + public class SkinnedMeshPart extends MeshPart { + private ComputeShaderSetup.MeshPartBuffer partVBO; + + public SkinnedMeshPart(List animatedMeshPartList, @Nullable Mesh.RenderProperties renderProperties, @Nullable Supplier vanillaPartTracer) { + super(animatedMeshPartList, renderProperties, vanillaPartTracer); + } + + public void initVBO(ComputeShaderSetup.MeshPartBuffer partVBO) { + this.partVBO = partVBO; + } + + public ComputeShaderSetup.MeshPartBuffer getPartVBO() { + return this.partVBO; + } + + @Override + public void draw(PoseStack poseStack, VertexConsumer bufferBuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) { + if (this.isHidden()) { + return; + } + + Vector4f color = this.getColor(r, g, b, a); + Matrix4f pose = poseStack.last().pose(); + Matrix3f normal = poseStack.last().normal(); + + for (VertexBuilder vi : this.getVertices()) { + getVertexPosition(vi.position, POSITION); + getVertexNormal(vi.normal, NORMAL); + POSITION.mul(pose); + NORMAL.mul(normal); + drawingFunction.draw(bufferBuilder, POSITION.x(), POSITION.y(), POSITION.z(), NORMAL.x(), NORMAL.y(), NORMAL.z(), packedLight, color.x, color.y, color.z, color.w, uvs[vi.uv * 2], uvs[vi.uv * 2 + 1], overlay); + } + } + } + + /** + * Export this model as Json format + */ + public JsonObject toJsonObject() { + JsonObject root = new JsonObject(); + JsonObject vertices = new JsonObject(); + float[] positions = this.positions.clone(); + float[] normals = this.normals.clone(); + + for (int i = 0; i < positions.length / 3; i++) { + int k = i * 3; + Vec4f posVector = new Vec4f(positions[k], positions[k+1], positions[k+2], 1.0F); + posVector.transform(JsonAssetLoader.MINECRAFT_TO_BLENDER_COORD); + positions[k] = posVector.x; + positions[k+1] = posVector.y; + positions[k+2] = posVector.z; + } + + for (int i = 0; i < normals.length / 3; i++) { + int k = i * 3; + Vec4f normVector = new Vec4f(normals[k], normals[k+1], normals[k+2], 1.0F); + normVector.transform(JsonAssetLoader.MINECRAFT_TO_BLENDER_COORD); + normals[k] = normVector.x; + normals[k+1] = normVector.y; + normals[k+2] = normVector.z; + } + + IntList affectingJointAndWeightIndices = new IntArrayList(); + + for (int i = 0; i < this.affectingJointCounts.length; i++) { + for (int j = 0; j < this.affectingJointCounts[j]; j++) { + affectingJointAndWeightIndices.add(this.affectingJointIndices[i][j]); + affectingJointAndWeightIndices.add(this.affectingWeightIndices[i][j]); + } + } + + vertices.add("positions", ParseUtil.farrayToJsonObject(positions, 3)); + vertices.add("uvs", ParseUtil.farrayToJsonObject(this.uvs, 2)); + vertices.add("normals", ParseUtil.farrayToJsonObject(normals, 3)); + vertices.add("vcounts", ParseUtil.iarrayToJsonObject(this.affectingJointCounts, 1)); + vertices.add("weights", ParseUtil.farrayToJsonObject(this.weights, 1)); + vertices.add("vindices", ParseUtil.iarrayToJsonObject(affectingJointAndWeightIndices.toIntArray(), 1)); + + if (!this.parts.isEmpty()) { + JsonObject parts = new JsonObject(); + + for (Map.Entry partEntry : this.parts.entrySet()) { + IntList indicesArray = new IntArrayList(); + + for (VertexBuilder vertexIndicator : partEntry.getValue().getVertices()) { + indicesArray.add(vertexIndicator.position); + indicesArray.add(vertexIndicator.uv); + indicesArray.add(vertexIndicator.normal); + } + + parts.add(partEntry.getKey(), ParseUtil.iarrayToJsonObject(indicesArray.toIntArray(), 3)); + } + + vertices.add("parts", parts); + } else { + int i = 0; + int[] indices = new int[this.vertexCount * 3]; + + for (SkinnedMeshPart part : this.parts.values()) { + for (VertexBuilder vertexIndicator : part.getVertices()) { + indices[i * 3] = vertexIndicator.position; + indices[i * 3 + 1] = vertexIndicator.uv; + indices[i * 3 + 2] = vertexIndicator.normal; + i++; + } + } + + vertices.add("indices", ParseUtil.iarrayToJsonObject(indices, 3)); + } + + root.add("vertices", vertices); + + if (this.renderProperties != null) { + JsonObject renderProperties = new JsonObject(); + renderProperties.addProperty("texture_path", this.renderProperties.customTexturePath().toString()); + renderProperties.addProperty("transparent", this.renderProperties.isTransparent()); + root.add("render_properties", renderProperties); + } + + return root; + } +} diff --git a/src/main/java/com/tiedup/remake/rig/mesh/SoftBodyTranslatable.java b/src/main/java/com/tiedup/remake/rig/mesh/SoftBodyTranslatable.java new file mode 100644 index 0000000..1604ba0 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/SoftBodyTranslatable.java @@ -0,0 +1,41 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.List; +import java.util.Map; + +import com.google.common.collect.Lists; + +import com.tiedup.remake.rig.mesh.Meshes.MeshAccessor; +import com.tiedup.remake.rig.cloth.ClothSimulatable; +import com.tiedup.remake.rig.cloth.ClothSimulator; +import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject.ClothPart.ConstraintType; +import yesman.epicfight.api.physics.SimulationProvider; + +public interface SoftBodyTranslatable extends SimulationProvider { + public static final List TRACKING_SIMULATION_SUBJECTS = Lists.newArrayList(); + + default boolean canStartSoftBodySimulation() { + return this.getSoftBodySimulationInfo() != null; + } + + void putSoftBodySimulationInfo(Map sofyBodySimulationInfo); + + Map getSoftBodySimulationInfo(); + + default StaticMesh getOriginalMesh() { + if (this instanceof MeshAccessor meshAccessor) { + return (StaticMesh)meshAccessor.get(); + } else { + return (StaticMesh)this; + } + } + + public static record ClothSimulationInfo(float particleMass, float selfCollision, List constraints, ConstraintType[] constraintTypes, float[] compliances, int[] particles, float[] weights, float[] rootDistance, int[] normalOffsetMapping) { + } +} diff --git a/src/main/java/com/tiedup/remake/rig/mesh/StaticMesh.java b/src/main/java/com/tiedup/remake/rig/mesh/StaticMesh.java new file mode 100644 index 0000000..ec06507 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/StaticMesh.java @@ -0,0 +1,150 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import org.joml.Vector3f; +import org.joml.Vector4f; + +import com.google.common.collect.ImmutableList; + +import net.minecraft.world.phys.Vec3; +import com.tiedup.remake.rig.cloth.ClothSimulatable; +import com.tiedup.remake.rig.cloth.ClothSimulator; +import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject; +import com.tiedup.remake.rig.util.ParseUtil; +import com.tiedup.remake.rig.math.OpenMatrix4f; + +public abstract class StaticMesh

implements Mesh, SoftBodyTranslatable { + protected final float[] positions; + protected final float[] normals; + protected final float[] uvs; + + protected final int vertexCount; + protected final Mesh.RenderProperties renderProperties; + protected final Map parts; + protected final List normalList; + + private Map softBodySimulationInfo; + + /** + * @param arrayMap Null if parent is not null + * @param partBuilders Null if parent is not null + * @param parent Null if arrayMap and parts are not null + * @param renderProperties + */ + public StaticMesh(@Nullable Map arrayMap, @Nullable Map> partBuilders, @Nullable StaticMesh

parent, Mesh.RenderProperties renderProperties) { + this.positions = (parent == null) ? ParseUtil.unwrapFloatWrapperArray(arrayMap.get("positions")) : parent.positions; + this.normals = (parent == null) ? ParseUtil.unwrapFloatWrapperArray(arrayMap.get("normals")) : parent.normals; + this.uvs = (parent == null) ? ParseUtil.unwrapFloatWrapperArray(arrayMap.get("uvs")) : parent.uvs; + this.parts = (parent == null) ? this.createModelPart(partBuilders) : parent.parts; + this.renderProperties = renderProperties; + + int totalV = 0; + + for (MeshPart modelpart : this.parts.values()) { + totalV += modelpart.getVertices().size(); + } + + this.vertexCount = totalV; + + if (this.canStartSoftBodySimulation()) { + ImmutableList.Builder normalBuilder = ImmutableList.builder(); + + for (int i = 0; i < this.normals.length / 3; i++) { + normalBuilder.add(new Vec3(this.normals[i * 3], this.normals[i * 3 + 1], this.normals[i * 3 + 2])); + } + + this.normalList = normalBuilder.build(); + } else { + this.normalList = null; + } + } + + protected abstract Map createModelPart(Map> partBuilders); + protected abstract P getOrLogException(Map parts, String name); + + public boolean hasPart(String part) { + return this.parts.containsKey(part); + } + + public MeshPart getPart(String part) { + return this.parts.get(part); + } + + public Collection

getAllParts() { + return this.parts.values(); + } + + public Set> getPartEntry() { + return this.parts.entrySet(); + } + + public void putSoftBodySimulationInfo(Map sofyBodySimulationInfo) { + this.softBodySimulationInfo = sofyBodySimulationInfo; + } + + public Map getSoftBodySimulationInfo() { + return this.softBodySimulationInfo; + } + + public Mesh.RenderProperties getRenderProperties() { + return this.renderProperties; + } + + public void getVertexPosition(int positionIndex, Vector4f dest) { + int index = positionIndex * 3; + dest.set(this.positions[index], this.positions[index + 1], this.positions[index + 2], 1.0F); + } + + public void getVertexNormal(int normalIndex, Vector3f dest) { + int index = normalIndex * 3; + dest.set(this.normals[index], this.normals[index + 1], this.normals[index + 2]); + } + + public void getVertexPosition(int positionIndex, Vector4f dest, @Nullable OpenMatrix4f[] poses) { + this.getVertexPosition(positionIndex, dest); + } + + public void getVertexNormal(int positionIndex, int normalIndex, Vector3f dest, @Nullable OpenMatrix4f[] poses) { + this.getVertexNormal(normalIndex, dest); + } + + public float[] positions() { + return this.positions; + } + + public float[] normals() { + return this.normals; + } + + public float[] uvs() { + return this.uvs; + } + + @Nullable + public List normalList() { + return this.normalList; + } + + @Override + public void initialize() { + this.parts.values().forEach((part) -> part.setHidden(false)); + } + + @SuppressWarnings("unchecked") + @Override + public ClothSimulator.ClothObject createSimulationData(@Nullable SoftBodyTranslatable provider, ClothSimulatable simObject, ClothSimulator.ClothObjectBuilder simBuilder) { + return new ClothObject(simBuilder, provider == null ? this : provider, (Map)this.parts, this.positions); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/mesh/VertexBuilder.java b/src/main/java/com/tiedup/remake/rig/mesh/VertexBuilder.java new file mode 100644 index 0000000..65e406d --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/mesh/VertexBuilder.java @@ -0,0 +1,62 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.mesh; + +import java.util.List; + +import com.google.common.collect.Lists; + + + +// Vertex Indices +public class VertexBuilder { + public static List create(int[] drawingIndices) { + List vertexIndicators = Lists.newArrayList(); + + for (int i = 0; i < drawingIndices.length / 3; i++) { + int k = i * 3; + int position = drawingIndices[k]; + int uv = drawingIndices[k + 1]; + int normal = drawingIndices[k + 2]; + VertexBuilder vi = new VertexBuilder(position, uv, normal); + vertexIndicators.add(vi); + } + + return vertexIndicators; + } + + public final int position; + public final int uv; + public final int normal; + + public VertexBuilder(int position, int uv, int normal) { + this.position = position; + this.uv = uv; + this.normal = normal; + } + + @Override + public boolean equals(Object o) { + if (o instanceof VertexBuilder vb) { + return this.position == vb.position && this.uv == vb.uv && this.normal == vb.normal; + } + + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + this.position; + result = prime * result + this.uv; + result = prime * result + this.normal; + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/patch/ClientPlayerPatch.java b/src/main/java/com/tiedup/remake/rig/patch/ClientPlayerPatch.java new file mode 100644 index 0000000..c093232 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/patch/ClientPlayerPatch.java @@ -0,0 +1,519 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.patch; + +import java.util.Optional; + +import org.joml.Vector4f; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.PlayerRideableJumping; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.UseAnim; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.EntityJoinLevelEvent; +import net.minecraftforge.event.entity.living.LivingEvent; +import com.tiedup.remake.rig.anim.Animator; +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.anim.LivingMotion; +import com.tiedup.remake.rig.anim.LivingMotions; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty; +import com.tiedup.remake.rig.anim.types.ActionAnimation; +import com.tiedup.remake.rig.anim.types.DynamicAnimation; +import com.tiedup.remake.rig.anim.client.ClientAnimator; +import com.tiedup.remake.rig.anim.client.Layer; +import com.tiedup.remake.rig.event.RenderEpicFightPlayerEvent; +import com.tiedup.remake.rig.event.UpdatePlayerMotionEvent; +import yesman.epicfight.client.online.EpicSkins; +import com.tiedup.remake.rig.cloth.ClothSimulatable; +import com.tiedup.remake.rig.cloth.ClothSimulator; +import yesman.epicfight.api.physics.PhysicsSimulator; +import yesman.epicfight.api.physics.SimulationTypes; +import com.tiedup.remake.rig.util.EntitySnapshot; +import com.tiedup.remake.rig.math.MathUtils; +import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.rig.math.Vec3f; +import yesman.epicfight.config.ClientConfig; +import yesman.epicfight.gameasset.EpicFightSounds; +import yesman.epicfight.network.EntityPairingPacketTypes; +import yesman.epicfight.network.server.SPEntityPairingPacket; +import yesman.epicfight.particle.EpicFightParticles; +import com.tiedup.remake.rig.patch.EntityDecorations; +import com.tiedup.remake.rig.patch.EntityDecorations.RenderAttributeModifier; +import com.tiedup.remake.rig.patch.PlayerPatch; +import com.tiedup.remake.rig.patch.item.CapabilityItem; +import yesman.epicfight.world.entity.eventlistener.PlayerEventListener.EventType; + +public class AbstractClientPlayerPatch extends PlayerPatch implements ClothSimulatable { + private Item prevHeldItem; + private Item prevHeldItemOffHand; + protected EpicSkins epicSkinsInformation; + + @Override + public void onJoinWorld(T entity, EntityJoinLevelEvent event) { + super.onJoinWorld(entity, event); + + this.prevHeldItem = Items.AIR; + this.prevHeldItemOffHand = Items.AIR; + + EpicSkins.initEpicSkins(this); + } + + @Override + public void updateMotion(boolean considerInaction) { + if (this.original.getHealth() <= 0.0F) { + currentLivingMotion = LivingMotions.DEATH; + } else if (!this.state.updateLivingMotion() && considerInaction) { + currentLivingMotion = LivingMotions.INACTION; + } else { + if (original.isFallFlying() || original.isAutoSpinAttack()) { + currentLivingMotion = LivingMotions.FLY; + } else if (original.getVehicle() != null) { + if (original.getVehicle() instanceof PlayerRideableJumping) + currentLivingMotion = LivingMotions.MOUNT; + else + currentLivingMotion = LivingMotions.SIT; + } else if (original.isVisuallySwimming()) { + currentLivingMotion = LivingMotions.SWIM; + } else if (original.isSleeping()) { + currentLivingMotion = LivingMotions.SLEEP; + } else if (!original.onGround() && original.onClimbable()) { + currentLivingMotion = LivingMotions.CLIMB; + } else if (!original.getAbilities().flying) { + ClientAnimator animator = this.getClientAnimator(); + + if (original.isUnderWater() && (original.getY() - this.yo) < -0.005) + currentLivingMotion = LivingMotions.FLOAT; + else if (original.getY() - this.yo < -0.4F || this.isAirborneState()) + currentLivingMotion = LivingMotions.FALL; + else if (this.isMoving()) { + if (original.isCrouching()) + currentLivingMotion = LivingMotions.SNEAK; + else if (original.isSprinting()) + currentLivingMotion = LivingMotions.RUN; + else + currentLivingMotion = LivingMotions.WALK; + + animator.baseLayer.animationPlayer.setReversed(this.dz < 0); + + } else { + animator.baseLayer.animationPlayer.setReversed(false); + + if (original.isCrouching()) + currentLivingMotion = LivingMotions.KNEEL; + else + currentLivingMotion = LivingMotions.IDLE; + } + } else { + if (this.isMoving()) + currentLivingMotion = LivingMotions.CREATIVE_FLY; + else + currentLivingMotion = LivingMotions.CREATIVE_IDLE; + } + } + + UpdatePlayerMotionEvent.BaseLayer baseLayerEvent = new UpdatePlayerMotionEvent.BaseLayer(this, this.currentLivingMotion, !this.state.updateLivingMotion() && considerInaction); + this.eventListeners.triggerEvents(EventType.UPDATE_BASE_LIVING_MOTION_EVENT, baseLayerEvent); + MinecraftForge.EVENT_BUS.post(baseLayerEvent); + + this.currentLivingMotion = baseLayerEvent.getMotion(); + + if (!this.state.updateLivingMotion() && considerInaction) { + this.currentCompositeMotion = LivingMotions.NONE; + } else { + CapabilityItem mainhandItemCap = this.getHoldingItemCapability(InteractionHand.MAIN_HAND); + CapabilityItem offhandItemCap = this.getHoldingItemCapability(InteractionHand.OFF_HAND); + LivingMotion customLivingMotion = mainhandItemCap.getLivingMotion(this, InteractionHand.MAIN_HAND); + + if (customLivingMotion == null) customLivingMotion = offhandItemCap.getLivingMotion(this, InteractionHand.OFF_HAND); + + // When item capabilities has custom living motion + if (customLivingMotion != null) + currentCompositeMotion = customLivingMotion; + else if (this.original.isUsingItem()) { + UseAnim useAnim = this.original.getUseItem().getUseAnimation(); + if (useAnim == UseAnim.BLOCK) + currentCompositeMotion = LivingMotions.BLOCK_SHIELD; + else if (useAnim == UseAnim.CROSSBOW) + currentCompositeMotion = LivingMotions.RELOAD; + else if (useAnim == UseAnim.DRINK) + currentCompositeMotion = LivingMotions.DRINK; + else if (useAnim == UseAnim.EAT) + currentCompositeMotion = LivingMotions.EAT; + else if (useAnim == UseAnim.SPYGLASS) + currentCompositeMotion = LivingMotions.SPECTATE; + else + currentCompositeMotion = currentLivingMotion; + } else { + if (this.getClientAnimator().getCompositeLayer(Layer.Priority.MIDDLE).animationPlayer.getRealAnimation().get().isReboundAnimation()) + currentCompositeMotion = LivingMotions.SHOT; + else if (this.original.swinging && this.original.getSleepingPos().isEmpty()) + currentCompositeMotion = LivingMotions.DIGGING; + else + currentCompositeMotion = currentLivingMotion; + } + + UpdatePlayerMotionEvent.CompositeLayer compositeLayerEvent = new UpdatePlayerMotionEvent.CompositeLayer(this, this.currentCompositeMotion); + this.eventListeners.triggerEvents(EventType.UPDATE_COMPOSITE_LIVING_MOTION_EVENT, compositeLayerEvent); + MinecraftForge.EVENT_BUS.post(compositeLayerEvent); + + this.currentCompositeMotion = compositeLayerEvent.getMotion(); + } + } + + @Override + public void onOldPosUpdate() { + this.modelYRotO2 = this.modelYRotO; + this.xPosO2 = (float)this.original.xOld; + this.yPosO2 = (float)this.original.yOld; + this.zPosO2 = (float)this.original.zOld; + } + + @Override + protected void clientTick(LivingEvent.LivingTickEvent event) { + this.xCloakO2 = this.original.xCloakO; + this.yCloakO2 = this.original.yCloakO; + this.zCloakO2 = this.original.zCloakO; + + super.clientTick(event); + + if (!this.getEntityState().updateLivingMotion()) { + this.original.yBodyRot = this.original.yHeadRot; + } + + boolean isMainHandChanged = this.prevHeldItem != this.original.getInventory().getSelected().getItem(); + boolean isOffHandChanged = this.prevHeldItemOffHand != this.original.getInventory().offhand.get(0).getItem(); + + if (isMainHandChanged || isOffHandChanged) { + this.updateHeldItem(this.getHoldingItemCapability(InteractionHand.MAIN_HAND), this.getHoldingItemCapability(InteractionHand.OFF_HAND)); + + if (isMainHandChanged) { + this.prevHeldItem = this.original.getInventory().getSelected().getItem(); + } + + if (isOffHandChanged) { + this.prevHeldItemOffHand = this.original.getInventory().offhand.get(0).getItem(); + } + } + + /** {@link LivingDeathEvent} never fired for client players **/ + if (this.original.deathTime == 1) { + this.getClientAnimator().playDeathAnimation(); + } + + this.clothSimulator.tick(this); + } + + protected boolean isMoving() { + return Math.abs(this.dx) > 0.01F || Math.abs(this.dz) > 0.01F; + } + + public void updateHeldItem(CapabilityItem mainHandCap, CapabilityItem offHandCap) { + this.cancelItemUse(); + + this.getClientAnimator().iterAllLayers((layer) -> { + if (layer.isOff()) { + return; + } + + layer.animationPlayer.getRealAnimation().get().getProperty(StaticAnimationProperty.ON_ITEM_CHANGE_EVENT).ifPresent((event) -> { + event.params(mainHandCap, offHandCap); + event.execute(this, layer.animationPlayer.getRealAnimation(), layer.animationPlayer.getPrevElapsedTime(), layer.animationPlayer.getElapsedTime()); + }); + }); + } + + @Override + public void entityPairing(SPEntityPairingPacket packet) { + super.entityPairing(packet); + + if (packet.getPairingPacketType().is(EntityPairingPacketTypes.class)) { + switch (packet.getPairingPacketType().toEnum(EntityPairingPacketTypes.class)) { + case TECHNICIAN_ACTIVATED -> { + this.original.level().addParticle(EpicFightParticles.WHITE_AFTERIMAGE.get(), this.original.getX(), this.original.getY(), this.original.getZ(), Double.longBitsToDouble(this.original.getId()), 0, 0); + } + case ADRENALINE_ACTIVATED -> { + if (this.original.isLocalPlayer()) { + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(EpicFightSounds.ADRENALINE.get(), 1.0F, 1.0F)); + } else { + this.original.playSound(EpicFightSounds.ADRENALINE.get()); + } + + this.original.level().addParticle(EpicFightParticles.ADRENALINE_PLAYER_BEATING.get(), this.original.getX(), this.original.getY(), this.original.getZ(), Double.longBitsToDouble(this.original.getId()), 0, 0); + } + case EMERGENCY_ESCAPE_ACTIVATED -> { + float yRot = packet.getBuffer().readFloat(); + this.original.level().addParticle(EpicFightParticles.AIR_BURST.get(), this.original.getX(), this.original.getY() + this.original.getBbHeight() * 0.5F, this.original.getZ(), 90.0F, yRot, 0); + + this.entityDecorations.addColorModifier(EntityDecorations.EMERGENCY_ESCAPE_TRANSPARENCY_MODIFIER, new RenderAttributeModifier<> () { + private int tickCount; + + @Override + public void modifyValue(Vector4f val, float partialTick) { + val.w = (float)Math.pow((this.tickCount + partialTick) / 6.0D, 2.0D) - 0.4F; + } + + @Override + public boolean shouldRemove() { + return this.tickCount > 6; + } + + @Override + public void tick() { + ++this.tickCount; + } + }); + } + } + } + } + + @Override + public boolean overrideRender() { + RenderEpicFightPlayerEvent renderepicfightplayerevent = new RenderEpicFightPlayerEvent(this, !ClientConfig.enableOriginalModel || this.isEpicFightMode()); + MinecraftForge.EVENT_BUS.post(renderepicfightplayerevent); + return renderepicfightplayerevent.getShouldRender(); + } + + @Override + public boolean shouldMoveOnCurrentSide(ActionAnimation actionAnimation) { + return false; + } + + @Override + public void poseTick(DynamicAnimation animation, Pose pose, float elapsedTime, float partialTick) { + if (pose.hasTransform("Head") && this.armature.hasJoint("Head")) { + if (animation.doesHeadRotFollowEntityHead()) { + float headRelativeRot = Mth.rotLerp(partialTick, Mth.wrapDegrees(this.modelYRotO - this.original.yHeadRotO), Mth.wrapDegrees(this.modelYRot - this.original.yHeadRot)); + OpenMatrix4f headTransform = this.armature.getBoundTransformFor(pose, this.armature.searchJointByName("Head")); + OpenMatrix4f toOriginalRotation = headTransform.removeScale().removeTranslation().invert(); + Vec3f xAxis = OpenMatrix4f.transform3v(toOriginalRotation, Vec3f.X_AXIS, null); + Vec3f yAxis = OpenMatrix4f.transform3v(toOriginalRotation, Vec3f.Y_AXIS, null); + OpenMatrix4f headRotation = OpenMatrix4f.createRotatorDeg(headRelativeRot, yAxis).rotateDeg(-Mth.rotLerp(partialTick, this.original.xRotO, this.original.getXRot()), xAxis); + pose.orElseEmpty("Head").frontResult(JointTransform.fromMatrix(headRotation), OpenMatrix4f::mul); + } + } + } + + @Override + public OpenMatrix4f getModelMatrix(float partialTick) { + if (this.original.isAutoSpinAttack()) { + OpenMatrix4f mat = MathUtils.getModelMatrixIntegral(0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0, 0, 0, 0, partialTick, PLAYER_SCALE, PLAYER_SCALE, PLAYER_SCALE); + float yRot = MathUtils.lerpBetween(this.original.yRotO, this.original.getYRot(), partialTick); + float xRot = MathUtils.lerpBetween(this.original.xRotO, this.original.getXRot(), partialTick); + + mat.rotateDeg(-yRot, Vec3f.Y_AXIS) + .rotateDeg(-xRot, Vec3f.X_AXIS) + .rotateDeg((this.original.tickCount + partialTick) * -55.0F, Vec3f.Z_AXIS) + .translate(0F, -0.39F, 0F); + + return mat; + } else if (this.original.isFallFlying()) { + OpenMatrix4f mat = MathUtils.getModelMatrixIntegral(0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0, 0, 0, 0, partialTick, PLAYER_SCALE, PLAYER_SCALE, PLAYER_SCALE); + float f1 = (float)this.original.getFallFlyingTicks() + partialTick; + float f2 = Mth.clamp(f1 * f1 / 100.0F, 0.0F, 1.0F); + + mat.rotateDeg(-Mth.rotLerp(partialTick, this.original.yBodyRotO, this.original.yBodyRot), Vec3f.Y_AXIS).rotateDeg(f2 * (-this.original.getXRot()), Vec3f.X_AXIS); + + Vec3 vec3d = this.original.getViewVector(partialTick); + Vec3 vec3d1 = this.original.getDeltaMovementLerped(partialTick); + double d0 = vec3d1.horizontalDistanceSqr(); + double d1 = vec3d.horizontalDistanceSqr(); + + if (d0 > 0.0D && d1 > 0.0D) { + double d2 = (vec3d1.x * vec3d.x + vec3d1.z * vec3d.z) / (Math.sqrt(d0) * Math.sqrt(d1)); + double d3 = vec3d1.x * vec3d.z - vec3d1.z * vec3d.x; + mat.rotate((float)-((Math.signum(d3) * Math.acos(d2))), Vec3f.Z_AXIS); + } + + return mat; + + } else if (this.original.isSleeping()) { + BlockState blockstate = this.original.getFeetBlockState(); + float yRot = 0.0F; + + if (blockstate.isBed(this.original.level(), this.original.getSleepingPos().orElse(null), this.original)) { + if (blockstate.hasProperty(BlockStateProperties.HORIZONTAL_FACING)) { + switch(blockstate.getValue(BlockStateProperties.HORIZONTAL_FACING)) { + case EAST: + yRot = 90.0F; + break; + case WEST: + yRot = -90.0F; + break; + case SOUTH: + yRot = 180.0F; + break; + default: + break; + } + } + } + + return MathUtils.getModelMatrixIntegral(0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, yRot, yRot, 0, PLAYER_SCALE, PLAYER_SCALE, PLAYER_SCALE); + } else { + float yRotO; + float yRot; + float xRotO = 0; + float xRot = 0; + + if (this.original.getVehicle() instanceof LivingEntity ridingEntity) { + yRotO = ridingEntity.yBodyRotO; + yRot = ridingEntity.yBodyRot; + } else { + yRotO = this.modelYRotO; + yRot = this.modelYRot; + } + + if (!this.getEntityState().inaction() && this.original.getPose() == net.minecraft.world.entity.Pose.SWIMMING) { + float f = this.original.getSwimAmount(partialTick); + float f3 = this.original.isInWater() ? this.original.getXRot() : 0; + float f4 = Mth.lerp(f, 0.0F, f3); + xRotO = f4; + xRot = f4; + } + + return MathUtils.getModelMatrixIntegral(0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, xRotO, xRot, yRotO, yRot, partialTick, PLAYER_SCALE, PLAYER_SCALE, PLAYER_SCALE); + } + } + + public void setEpicSkinsInformation(EpicSkins epicSkinsInformation) { + this.epicSkinsInformation = epicSkinsInformation; + } + + public EpicSkins getEpicSkinsInformation() { + return this.epicSkinsInformation; + } + + public boolean isEpicSkinsLoaded() { + return this.epicSkinsInformation != null; + } + + @Override + public EntitySnapshot captureEntitySnapshot() { + return EntitySnapshot.capturePlayer(this); + } + + private final ClothSimulator clothSimulator = new ClothSimulator(); + public float modelYRotO2; + public double xPosO2; + public double yPosO2; + public double zPosO2; + public double xCloakO2; + public double yCloakO2; + public double zCloakO2; + + @SuppressWarnings("unchecked") + @Override + public > Optional getSimulator(SimulationTypes simulationType) { + if (simulationType == SimulationTypes.CLOTH) { + return Optional.of((SIM)this.clothSimulator); + } + + return Optional.empty(); + } + + @Override + public ClothSimulator getClothSimulator() { + return this.clothSimulator; + } + + @Override + public Vec3 getAccurateCloakLocation(float partialFrame) { + if (partialFrame < 0.0F) { + partialFrame = 1.0F - partialFrame; + + double x = Mth.lerp((double)partialFrame, this.xCloakO2, this.original.xCloakO) - Mth.lerp((double)partialFrame, this.xPosO2, this.original.xo); + double y = Mth.lerp((double)partialFrame, this.yCloakO2, this.original.yCloakO) - Mth.lerp((double)partialFrame, this.yPosO2, this.original.yo); + double z = Mth.lerp((double)partialFrame, this.zCloakO2, this.original.zCloakO) - Mth.lerp((double)partialFrame, this.zPosO2, this.original.zo); + + return new Vec3(x, y, z); + } else { + double x = Mth.lerp((double)partialFrame, this.original.xCloakO, this.original.xCloak) - Mth.lerp((double)partialFrame, this.original.xo, this.original.getX()); + double y = Mth.lerp((double)partialFrame, this.original.yCloakO, this.original.yCloak) - Mth.lerp((double)partialFrame, this.original.yo, this.original.getY()); + double z = Mth.lerp((double)partialFrame, this.original.zCloakO, this.original.zCloak) - Mth.lerp((double)partialFrame, this.original.zo, this.original.getZ()); + + return new Vec3(x, y, z); + } + } + + @Override + public Vec3 getAccuratePartialLocation(float partialFrame) { + if (partialFrame < 0.0F) { + partialFrame = 1.0F + partialFrame; + + double x = Mth.lerp((double)partialFrame, this.xPosO2, this.original.xOld); + double y = Mth.lerp((double)partialFrame, this.yPosO2, this.original.yOld); + double z = Mth.lerp((double)partialFrame, this.zPosO2, this.original.zOld); + + return new Vec3(x, y, z); + } else { + double x = Mth.lerp((double)partialFrame, this.original.xOld, this.original.getX()); + double y = Mth.lerp((double)partialFrame, this.original.yOld, this.original.getY()); + double z = Mth.lerp((double)partialFrame, this.original.zOld, this.original.getZ()); + + return new Vec3(x, y, z); + } + } + + @Override + public Vec3 getObjectVelocity() { + return new Vec3(this.original.getX() - this.original.xOld, this.original.getY() - this.original.yOld, this.original.getZ() - this.original.zOld); + } + + @Override + public float getAccurateYRot(float partialFrame) { + if (partialFrame < 0.0F) { + partialFrame = 1.0F + partialFrame; + + return Mth.rotLerp(partialFrame, this.modelYRotO2, this.getYRotO()); + } else { + return Mth.rotLerp(partialFrame, this.getYRotO(), this.getYRot()); + } + } + + @Override + public float getYRotDelta(float partialFrame) { + if (partialFrame < 0.0F) { + partialFrame = 1.0F + partialFrame; + + return Mth.rotLerp(partialFrame, this.modelYRotO2, this.getYRotO()) - this.modelYRotO2; + } else { + return Mth.rotLerp(partialFrame, this.getYRotO(), this.getYRot()) - this.getYRotO(); + } + } + + @Override + public boolean invalid() { + return this.original.isRemoved(); + } + + @Override + public float getScale() { + return PLAYER_SCALE; + } + + @Override + public Animator getSimulatableAnimator() { + return this.animator; + } + + @Override + public float getGravity() { + return this.getOriginal().isUnderWater() ? 0.98F : 9.8F; + } +} diff --git a/src/main/java/com/tiedup/remake/rig/patch/LocalPlayerPatch.java b/src/main/java/com/tiedup/remake/rig/patch/LocalPlayerPatch.java new file mode 100644 index 0000000..9d7131a --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/patch/LocalPlayerPatch.java @@ -0,0 +1,579 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.patch; + +import net.minecraft.client.CameraType; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.entity.PartEntity; +import net.minecraftforge.event.entity.EntityJoinLevelEvent; +import net.minecraftforge.event.entity.living.LivingEvent; +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.anim.Keyframe; +import com.tiedup.remake.rig.anim.Pose; +import com.tiedup.remake.rig.anim.TransformSheet; +import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty; +import com.tiedup.remake.rig.anim.types.ActionAnimation; +import com.tiedup.remake.rig.anim.types.AttackAnimation; +import com.tiedup.remake.rig.anim.types.DirectStaticAnimation; +import com.tiedup.remake.rig.anim.types.StaticAnimation; +import com.tiedup.remake.rig.asset.AssetAccessor; +import com.tiedup.remake.rig.anim.client.AnimationSubFileReader; +import com.tiedup.remake.rig.anim.client.AnimationSubFileReader.PovSettings; +import com.tiedup.remake.rig.anim.client.AnimationSubFileReader.PovSettings.ViewLimit; +import com.tiedup.remake.rig.anim.client.Layer; +import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties; +import yesman.epicfight.api.client.camera.EpicFightCameraAPI; +import yesman.epicfight.api.client.input.InputManager; +import yesman.epicfight.api.client.input.action.MinecraftInputAction; +import com.tiedup.remake.rig.math.MathUtils; +import yesman.epicfight.client.ClientEngine; +import yesman.epicfight.client.events.engine.RenderEngine; +import yesman.epicfight.client.gui.screen.SkillBookScreen; +import yesman.epicfight.config.ClientConfig; +import yesman.epicfight.gameasset.Animations; +import yesman.epicfight.main.EpicFightSharedConstants; +import yesman.epicfight.network.EpicFightNetworkManager; +import yesman.epicfight.network.client.CPAnimatorControl; +import yesman.epicfight.network.client.CPChangePlayerMode; +import yesman.epicfight.network.client.CPModifyEntityModelYRot; +import yesman.epicfight.network.client.CPSetStamina; +import yesman.epicfight.network.common.AnimatorControlPacket; +import yesman.epicfight.skill.modules.ChargeableSkill; +import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.patch.PlayerPatch; +import com.tiedup.remake.rig.patch.item.CapabilityItem; +import yesman.epicfight.world.entity.eventlistener.PlayerEventListener.EventType; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class LocalPlayerPatch extends AbstractClientPlayerPatch { + + private static final UUID ACTION_EVENT_UUID = UUID.fromString("d1a1e102-1621-11ed-861d-0242ac120002"); + private Minecraft minecraft; + private float staminaO; + private int prevChargingAmount; + + private AnimationSubFileReader.PovSettings povSettings; + private FirstPersonLayer firstPersonLayer = new FirstPersonLayer(); + + @Override + public void onConstructed(LocalPlayer entity) { + super.onConstructed(entity); + this.minecraft = Minecraft.getInstance(); + } + + @Override + public void onJoinWorld(LocalPlayer player, EntityJoinLevelEvent event) { + super.onJoinWorld(player, event); + + this.eventListeners.addEventListener(EventType.ACTION_EVENT_CLIENT, ACTION_EVENT_UUID, (playerEvent) -> { + ClientEngine.getInstance().controlEngine.unlockHotkeys(); + }); + } + + public void onRespawnLocalPlayer(ClientPlayerNetworkEvent.Clone event) { + this.onJoinWorld(event.getNewPlayer(), new EntityJoinLevelEvent(event.getNewPlayer(), event.getNewPlayer().level())); + } + + @Override + public void tick(LivingEvent.LivingTickEvent event) { + this.staminaO = this.getStamina(); + + if (this.isHoldingAny() && this.getHoldingSkill() instanceof ChargeableSkill) { + this.prevChargingAmount = this.getChargingAmount(); + } else { + this.prevChargingAmount = 0; + } + + super.tick(event); + } + + @Override + public void clientTick(LivingEvent.LivingTickEvent event) { + this.staminaO = this.getStamina(); + + super.clientTick(event); + + // Handle first person animation + final AssetAccessor currentPlaying = this.firstPersonLayer.animationPlayer.getRealAnimation(); + + boolean noPovAnimation = this.getClientAnimator().iterVisibleLayersUntilFalse(layer -> { + if (layer.isOff()) { + return true; + } + + Optional optPovAnimation = layer.animationPlayer.getRealAnimation().get().getProperty(ClientAnimationProperties.POV_ANIMATION); + Optional optPovSettings = layer.animationPlayer.getRealAnimation().get().getProperty(ClientAnimationProperties.POV_SETTINGS); + + optPovAnimation.ifPresent(povAnimation -> { + if (!povAnimation.equals(currentPlaying.get())) { + this.firstPersonLayer.playAnimation(povAnimation, layer.animationPlayer.getRealAnimation(), this, 0.0F); + this.povSettings = optPovSettings.get(); + } + }); + + return !optPovAnimation.isPresent(); + }); + + if (noPovAnimation && !currentPlaying.equals(Animations.EMPTY_ANIMATION)) { + this.firstPersonLayer.off(); + } + + this.firstPersonLayer.update(this); + + if (this.firstPersonLayer.animationPlayer.getAnimation().equals(Animations.EMPTY_ANIMATION)) { + this.povSettings = null; + } + } + + @Override + public boolean overrideRender() { + // Disable rendering the player when animated first person model disabled + if (this.original.is(this.minecraft.player)) { + if (this.minecraft.options.getCameraType().isFirstPerson() && !ClientConfig.enableAnimatedFirstPersonModel) { + return false; + } + } + + return super.overrideRender(); + } + + @Override + public LivingEntity getTarget() { + return EpicFightCameraAPI.getInstance().getFocusingEntity(); + } + + @Override + public void toVanillaMode(boolean synchronize) { + if (this.playerMode != PlayerMode.VANILLA) { + ClientEngine.getInstance().renderEngine.downSlideSkillUI(); + + if (ClientConfig.autoSwitchCamera) { + this.minecraft.options.setCameraType(CameraType.FIRST_PERSON); + } + + if (synchronize) { + EpicFightNetworkManager.sendToServer(new CPChangePlayerMode(PlayerMode.VANILLA)); + } + } + + super.toVanillaMode(synchronize); + } + + @Override + public void toEpicFightMode(boolean synchronize) { + if (this.playerMode != PlayerMode.EPICFIGHT) { + ClientEngine.getInstance().renderEngine.upSlideSkillUI(); + + if (ClientConfig.autoSwitchCamera) { + this.minecraft.options.setCameraType(CameraType.THIRD_PERSON_BACK); + } + + if (synchronize) { + EpicFightNetworkManager.sendToServer(new CPChangePlayerMode(PlayerMode.EPICFIGHT)); + } + } + + super.toEpicFightMode(synchronize); + } + + @Override + public boolean isFirstPerson() { + return this.minecraft.options.getCameraType() == CameraType.FIRST_PERSON; + } + + @Override + public boolean shouldBlockMoving() { + return InputManager.isActionActive(MinecraftInputAction.MOVE_BACKWARD) || InputManager.isActionActive(MinecraftInputAction.SNEAK); + } + + @Override + public boolean shouldMoveOnCurrentSide(ActionAnimation actionAnimation) { + if (!this.isLogicalClient()) { + return false; + } + + return actionAnimation.shouldPlayerMove(this); + } + + public float getStaminaO() { + return this.staminaO; + } + + public int getPrevChargingAmount() { + return this.prevChargingAmount; + } + + public FirstPersonLayer getFirstPersonLayer() { + return this.firstPersonLayer; + } + + public AnimationSubFileReader.PovSettings getPovSettings() { + return this.povSettings; + } + + public boolean hasCameraAnimation() { + return this.povSettings != null && this.povSettings.cameraTransform() != null; + } + + @Override + public void setStamina(float value) { + EpicFightNetworkManager.sendToServer(new CPSetStamina(value, true)); + } + + @Override + public void setModelYRot(float amount, boolean sendPacket) { + super.setModelYRot(amount, sendPacket); + + if (sendPacket) { + EpicFightNetworkManager.sendToServer(new CPModifyEntityModelYRot(amount)); + } + } + + public float getModelYRot() { + return this.modelYRot; + } + + public void setModelYRotInGui(float rotDeg) { + this.useModelYRot = true; + this.modelYRot = rotDeg; + } + + public void disableModelYRotInGui(float originalDeg) { + this.useModelYRot = false; + this.modelYRot = originalDeg; + } + + @Override + public void disableModelYRot(boolean sendPacket) { + super.disableModelYRot(sendPacket); + + if (sendPacket) { + EpicFightNetworkManager.sendToServer(new CPModifyEntityModelYRot()); + } + } + + @Override + public double checkXTurn(double xRot) { + if (xRot == 0.0D) { + return xRot; + } + + if (ClientConfig.enablePovAction && this.minecraft.options.getCameraType().isFirstPerson() && this.isEpicFightMode() && !this.getFirstPersonLayer().isOff()) { + ViewLimit viewLimit = this.getPovSettings().viewLimit(); + + if (viewLimit != null) { + float xRotDest = this.original.getXRot() + (float)xRot * 0.15F; + + if (xRotDest <= viewLimit.xRotMin() || xRotDest >= viewLimit.xRotMax()) { + return 0.0D; + } + } + } + + return xRot; + } + + @Override + public double checkYTurn(double yRot) { + if (yRot == 0.0D) { + return yRot; + } + + if (ClientConfig.enablePovAction && this.minecraft.options.getCameraType().isFirstPerson() && this.isEpicFightMode() && !this.getFirstPersonLayer().isOff()) { + ViewLimit viewLimit = this.getPovSettings().viewLimit(); + + if (viewLimit != null) { + float yCamera = Mth.wrapDegrees(this.original.getYRot()); + float yBody = MathUtils.findNearestRotation(yCamera, this.getYRot()); + float yRotDest = yCamera + (float)yRot * 0.15F; + float yRotClamped = Mth.clamp(yRotDest, yBody + viewLimit.yRotMin(), yBody + viewLimit.yRotMax()); + + if (yRotDest != yRotClamped) { + return 0.0D; + } + } + } + + return yRot; + } + + @Override + public void beginAction(ActionAnimation animation) { + EpicFightCameraAPI cameraApi = EpicFightCameraAPI.getInstance(); + + if (cameraApi.isTPSMode()) { + if (cameraApi.getFocusingEntity() != null && animation instanceof AttackAnimation) { + cameraApi.alignPlayerLookToCrosshair(false, true, true); + } else { + cameraApi.alignPlayerLookToCameraRotation(false, true, true); + } + } + + if (!this.useModelYRot || animation.getProperty(ActionAnimationProperty.SYNC_CAMERA).orElse(false)) { + this.modelYRot = this.original.getYRot(); + } + + if (cameraApi.getFocusingEntity() != null && cameraApi.isLockingOnTarget() && !cameraApi.getFocusingEntity().isRemoved()) { + Vec3 playerPosition = this.original.position(); + Vec3 targetPosition = cameraApi.getFocusingEntity().position(); + Vec3 toTarget = targetPosition.subtract(playerPosition); + this.original.setYRot((float)MathUtils.getYRotOfVector(toTarget)); + } + } + + /** + * Play an animation after the current animation is finished + * @param animation + */ + @Override + public void reserveAnimation(AssetAccessor animation) { + this.animator.reserveAnimation(animation); + EpicFightNetworkManager.sendToServer(new CPAnimatorControl(AnimatorControlPacket.Action.RESERVE, animation, 0.0F, false, false, false)); + } + + /** + * Play an animation without convert time + * @param animation + */ + @Override + public void playAnimationInstantly(AssetAccessor animation) { + this.animator.playAnimationInstantly(animation); + EpicFightNetworkManager.sendToServer(new CPAnimatorControl(AnimatorControlPacket.Action.PLAY_INSTANTLY, animation, 0.0F, false, false, false)); + } + + /** + * Play a shooting animation to end aim pose + * This method doesn't send packet from client to server + */ + @Override + public void playShootingAnimation() { + this.animator.playShootingAnimation(); + EpicFightNetworkManager.sendToServer(new CPAnimatorControl(AnimatorControlPacket.Action.SHOT, -1, 0.0F, false, true, false)); + } + + /** + * Stop playing an animation + * @param animation + * @param transitionTimeModifier + */ + @Override + public void stopPlaying(AssetAccessor animation) { + this.animator.stopPlaying(animation); + EpicFightNetworkManager.sendToServer(new CPAnimatorControl(AnimatorControlPacket.Action.STOP, animation, -1.0F, false, false, false)); + } + + /** + * Play an animation ensuring synchronization between client-server + * Plays animation when getting response from server if it called in client side. + * Do not call this in client side for non-player entities. + * + * @param animation + * @param transitionTimeModifier + */ + @Override + public void playAnimationSynchronized(AssetAccessor animation, float transitionTimeModifier) { + EpicFightNetworkManager.sendToServer(new CPAnimatorControl(AnimatorControlPacket.Action.PLAY, animation, transitionTimeModifier, false, false, true)); + } + + /** + * Play an animation only in client side, including all clients tracking this entity + * @param animation + * @param convertTimeModifier + */ + @Override + public void playAnimationInClientSide(AssetAccessor animation, float transitionTimeModifier) { + this.animator.playAnimation(animation, transitionTimeModifier); + EpicFightNetworkManager.sendToServer(new CPAnimatorControl(AnimatorControlPacket.Action.PLAY, animation, transitionTimeModifier, false, true, false)); + } + + /** + * Pause an animator until it receives a proper order + * @param action SOFT_PAUSE: resume when next animation plays + * HARD_PAUSE: resume when hard pause is set false + * @param pause + **/ + @Override + public void pauseAnimator(AnimatorControlPacket.Action action, boolean pause) { + super.pauseAnimator(action, pause); + EpicFightNetworkManager.sendToServer(new CPAnimatorControl(action, -1, 0.0F, pause, false, false)); + } + + @Override + public void openSkillBook(ItemStack itemstack, InteractionHand hand) { + if (itemstack.hasTag() && itemstack.getTag().contains("skill")) { + Minecraft.getInstance().setScreen(new SkillBookScreen(this.original, itemstack, hand)); + } + } + + @Override + public void resetHolding() { + if (this.holdingSkill != null) { + ClientEngine.getInstance().controlEngine.releaseAllServedKeys(); + } + + super.resetHolding(); + } + + @Override + public void updateHeldItem(CapabilityItem mainHandCap, CapabilityItem offHandCap) { + super.updateHeldItem(mainHandCap, offHandCap); + + if (!ClientConfig.preferenceWork.checkHitResult()) { + if (ClientConfig.combatPreferredItems.contains(this.original.getMainHandItem().getItem())) { + this.toEpicFightMode(true); + } else if (ClientConfig.miningPreferredItems.contains(this.original.getMainHandItem().getItem())) { + this.toVanillaMode(true); + } + } + } + + /** + * Judge the next behavior depending on player's item preference and where he's looking at + * @return true if the next action is swing a weapon, false if the next action is breaking a block + */ + public boolean canPlayAttackAnimation() { + if (this.isVanillaMode()) { + return false; + } + + EpicFightCameraAPI cameraApi = EpicFightCameraAPI.getInstance(); + + HitResult hitResult = + (EpicFightCameraAPI.getInstance().isTPSMode() && cameraApi.getCrosshairHitResult() != null && cameraApi.getCrosshairHitResult().getLocation().distanceToSqr(this.original.getEyePosition()) < this.original.getBlockReach() * this.original.getBlockReach()) + ? cameraApi.getCrosshairHitResult() : this.minecraft.hitResult; + + if (hitResult == null) { + return true; + } + + EntityHitResult entityHitResult = RenderEngine.asEntityHitResult(hitResult); + + if (entityHitResult != null) { + Entity hitEntity = entityHitResult.getEntity(); + + if (!(hitEntity instanceof LivingEntity) && !(hitEntity instanceof PartEntity)) { + return false; + } + } + + if (EpicFightCameraAPI.getInstance().isLockingOnTarget()) { + return true; + } + + if (ClientConfig.preferenceWork.checkHitResult()) { + if (ClientConfig.combatPreferredItems.contains(this.original.getMainHandItem().getItem())) { + BlockHitResult blockHitResult = RenderEngine.asBlockHitResult(this.minecraft.hitResult); + + if (blockHitResult != null && this.minecraft.level != null) { + BlockPos bp = blockHitResult.getBlockPos(); + BlockState bs = this.minecraft.level.getBlockState(bp); + return !this.original.getMainHandItem().getItem().canAttackBlock(bs, this.original.level(), bp, this.original) || !this.original.getMainHandItem().isCorrectToolForDrops(bs); + } + } else { + return RenderEngine.hitResultNotEquals(this.minecraft.hitResult, HitResult.Type.BLOCK); + } + + return true; + } else { + return this.getPlayerMode() == PlayerPatch.PlayerMode.EPICFIGHT; + } + } + + public class FirstPersonLayer extends Layer { + private TransformSheet linkCameraTransform = new TransformSheet(List.of(new Keyframe(0.0F, JointTransform.empty()), new Keyframe(Float.MAX_VALUE, JointTransform.empty()))); + + public FirstPersonLayer() { + super(null); + } + + public void playAnimation(AssetAccessor nextFirstPersonAnimation, AssetAccessor originalAnimation, LivingEntityPatch entitypatch, float transitionTimeModifier) { + Optional povSettings = originalAnimation.get().getProperty(ClientAnimationProperties.POV_SETTINGS); + + boolean hasPrevCameraAnimation = LocalPlayerPatch.this.povSettings != null && LocalPlayerPatch.this.povSettings.cameraTransform() != null; + boolean hasNextCameraAnimation = povSettings.isPresent() && povSettings.get().cameraTransform() != null; + + // Activate pov animation + if (hasPrevCameraAnimation || hasNextCameraAnimation) { + if (hasPrevCameraAnimation) { + this.linkCameraTransform.getKeyframes()[0].transform().copyFrom(LocalPlayerPatch.this.povSettings.cameraTransform().getInterpolatedTransform(this.animationPlayer.getElapsedTime())); + } else { + this.linkCameraTransform.getKeyframes()[0].transform().copyFrom(JointTransform.empty()); + } + + if (hasNextCameraAnimation) { + this.linkCameraTransform.getKeyframes()[1].transform().copyFrom(povSettings.get().cameraTransform().getKeyframes()[0].transform()); + } else { + this.linkCameraTransform.getKeyframes()[1].transform().clearTransform(); + } + + this.linkCameraTransform.getKeyframes()[1].setTime(nextFirstPersonAnimation.get().getTransitionTime()); + } + + super.playAnimation(nextFirstPersonAnimation, entitypatch, transitionTimeModifier); + } + + public void off() { + // Off camera animation + if (LocalPlayerPatch.this.povSettings != null && LocalPlayerPatch.this.povSettings.cameraTransform() != null) { + this.linkCameraTransform.getKeyframes()[0].transform().copyFrom(LocalPlayerPatch.this.povSettings.cameraTransform().getInterpolatedTransform(this.animationPlayer.getElapsedTime())); + this.linkCameraTransform.getKeyframes()[1].transform().copyFrom(JointTransform.empty()); + this.linkCameraTransform.getKeyframes()[1].setTime(EpicFightSharedConstants.GENERAL_ANIMATION_TRANSITION_TIME); + } + + super.off(LocalPlayerPatch.this); + } + + @Override + protected Pose getCurrentPose(LivingEntityPatch entitypatch) { + return this.animationPlayer.isEmpty() ? super.getCurrentPose(entitypatch) : this.animationPlayer.getCurrentPose(entitypatch, 0.0F); + } + + public TransformSheet getLinkCameraTransform() { + return this.linkCameraTransform; + } + } + + /** + * @deprecated Use {@link EpicFightCameraAPI#isLockingOnTarget()} instead + */ + @Deprecated(forRemoval = true) + public boolean isTargetLockedOn() { + return EpicFightCameraAPI.getInstance().isLockingOnTarget(); + } + + /** + * @deprecated Use {@link EpicFightCameraAPI#setLockOn(boolean)} instead + */ + @Deprecated(forRemoval = true) + public void setLockOn(boolean targetLockedOn) { + EpicFightCameraAPI.getInstance().setLockOn(targetLockedOn); + } + + /** + * @deprecated Use {@link EpicFightCameraAPI#toggleLockOn()} instead + */ + @Deprecated(forRemoval = true) + public void toggleLockOn() { + this.setLockOn(!EpicFightCameraAPI.getInstance().isLockingOnTarget()); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/render/TiedUpRenderTypes.java b/src/main/java/com/tiedup/remake/rig/render/TiedUpRenderTypes.java new file mode 100644 index 0000000..7b4d5ee --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/render/TiedUpRenderTypes.java @@ -0,0 +1,660 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.render; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.joml.Vector4f; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.VertexFormat; + +import net.minecraft.Util; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.inventory.InventoryMenu; +import yesman.epicfight.main.EpicFightMod; + +public final class EpicFightRenderTypes extends RenderType { + public static RenderType makeTriangulated(RenderType renderType) { + if (renderType.mode() == VertexFormat.Mode.TRIANGLES) { + return renderType; + } + + if (renderType instanceof CompositeRenderType compositeRenderType) { + return new CompositeRenderType(renderType.name, renderType.format, VertexFormat.Mode.TRIANGLES, renderType.bufferSize(), renderType.affectsCrumbling(), renderType.sortOnUpload, compositeRenderType.state); + } else { + return renderType; + } + } + + private static final BiFunction TRIANGULATED_OUTLINE = + Util.memoize((texLocation, cullStateShard) -> { + return RenderType.create( + EpicFightMod.prefix("outline"), + DefaultVertexFormat.POSITION_COLOR_TEX, + VertexFormat.Mode.TRIANGLES, + 256, + false, + false, + RenderType.CompositeState.builder() + .setShaderState(RENDERTYPE_OUTLINE_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(texLocation, false, false)) + .setCullState(cullStateShard) + .setDepthTestState(NO_DEPTH_TEST) + .setOutputState(OUTLINE_TARGET) + .createCompositeState(RenderType.OutlineProperty.IS_OUTLINE) + ); + }); + + private static final Map> TRIANGLED_RENDERTYPES_BY_NAME_TEXTURE = new HashMap<> (); + + private static final Function TRIANGULATED_RENDER_TYPES = Util.memoize(renderType -> { + if (renderType.mode() == VertexFormat.Mode.TRIANGLES) { + return renderType; + } + + if (renderType instanceof CompositeRenderType compositeRenderType) { + Optional cutoutTexture; + + if (compositeRenderType.state.textureState instanceof TextureStateShard texStateShard) { + cutoutTexture = texStateShard.texture; + } else { + cutoutTexture = Optional.empty(); + } + + if (TRIANGLED_RENDERTYPES_BY_NAME_TEXTURE.containsKey(renderType.name)) { + Map renderTypesByTexture = TRIANGLED_RENDERTYPES_BY_NAME_TEXTURE.get(renderType.name); + + if (compositeRenderType.state.textureState instanceof TextureStateShard) { + ResourceLocation texLocation = cutoutTexture.orElse(null); + + if (renderTypesByTexture.containsKey(texLocation)) { + return renderTypesByTexture.get(texLocation); + } + } + } + + CompositeRenderType triangulatedRenderType = new CompositeRenderType( + renderType.name, + renderType.format, + VertexFormat.Mode.TRIANGLES, + renderType.bufferSize(), + renderType.affectsCrumbling(), + renderType.sortOnUpload, + compositeRenderType.state + ); + + triangulatedRenderType.outline = triangulatedRenderType.outline.isEmpty() ? triangulatedRenderType.outline : cutoutTexture.map(texLocation -> { + return TRIANGULATED_OUTLINE.apply(texLocation, compositeRenderType.state.cullState); + }); + + return triangulatedRenderType; + } else { + return renderType; + } + }); + + public static RenderType getTriangulated(RenderType renderType) { + return TRIANGULATED_RENDER_TYPES.apply(renderType); + } + + /** + * Cache all Texture - RenderType entries to replace texture by MeshPart + */ + public static void addRenderType(String name, ResourceLocation textureLocation, RenderType renderType) { + Map renderTypesByTexture = TRIANGLED_RENDERTYPES_BY_NAME_TEXTURE.computeIfAbsent(name, (k) -> Maps.newHashMap()); + renderTypesByTexture.put(textureLocation, renderType); + } + + // Custom shards + protected static final RenderStateShard.ShaderStateShard PARTICLE_SHADER = new RenderStateShard.ShaderStateShard(GameRenderer::getParticleShader); + + public static class ShaderColorStateShard extends RenderStateShard { + private Vector4f color; + + public ShaderColorStateShard(Vector4f color) { + super( + "shader_color", + () -> { + RenderSystem.setShaderColor(color.x, color.y, color.z, color.w); + }, + () -> { + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + } + ); + + this.color = color; + } + + public void setColor(float r, float g, float b, float a) { + this.color.set(r, g, b, a); + } + } + + public static class MutableCompositeState extends CompositeState { + private ShaderColorStateShard shaderColorState = new ShaderColorStateShard(new Vector4f(1.0F)); + + public MutableCompositeState( + EmptyTextureStateShard pTextureState, ShaderStateShard pShaderState, + TransparencyStateShard pTransparencyState, DepthTestStateShard pDepthState, CullStateShard pCullState, + LightmapStateShard pLightmapState, OverlayStateShard pOverlayState, LayeringStateShard pLayeringState, + OutputStateShard pOutputState, TexturingStateShard pTexturingState, WriteMaskStateShard pWriteMaskState, + LineStateShard pLineState, ColorLogicStateShard pColorLogicState, RenderType.OutlineProperty pOutlineProperty + ) { + super( + pTextureState, pShaderState, pTransparencyState, pDepthState, pCullState, pLightmapState, pOverlayState, + pLayeringState, pOutputState, pTexturingState, pWriteMaskState, pLineState, pColorLogicState, pOutlineProperty + ); + + List list = new ArrayList<> (this.states); + list.add(this.shaderColorState); + this.states = ImmutableList.copyOf(list); + } + + public void setShaderColor(int r, int g, int b, int a) { + this.shaderColorState.setColor(r / 255.0F, g / 255.0F, b / 255.0F, a / 255.0F); + } + + public void setShaderColor(float r, float g, float b, float a) { + this.shaderColorState.setColor(r, g, b, a); + } + + public static EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder mutableStateBuilder() { + return new EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder(); + } + + public static class MutableCompositeStateBuilder { + private RenderStateShard.EmptyTextureStateShard textureState = RenderStateShard.NO_TEXTURE; + private RenderStateShard.ShaderStateShard shaderState = RenderStateShard.NO_SHADER; + private RenderStateShard.TransparencyStateShard transparencyState = RenderStateShard.NO_TRANSPARENCY; + private RenderStateShard.DepthTestStateShard depthTestState = RenderStateShard.LEQUAL_DEPTH_TEST; + private RenderStateShard.CullStateShard cullState = RenderStateShard.CULL; + private RenderStateShard.LightmapStateShard lightmapState = RenderStateShard.NO_LIGHTMAP; + private RenderStateShard.OverlayStateShard overlayState = RenderStateShard.NO_OVERLAY; + private RenderStateShard.LayeringStateShard layeringState = RenderStateShard.NO_LAYERING; + private RenderStateShard.OutputStateShard outputState = RenderStateShard.MAIN_TARGET; + private RenderStateShard.TexturingStateShard texturingState = RenderStateShard.DEFAULT_TEXTURING; + private RenderStateShard.WriteMaskStateShard writeMaskState = RenderStateShard.COLOR_DEPTH_WRITE; + private RenderStateShard.LineStateShard lineState = RenderStateShard.DEFAULT_LINE; + private RenderStateShard.ColorLogicStateShard colorLogicState = RenderStateShard.NO_COLOR_LOGIC; + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setTextureState(RenderStateShard.EmptyTextureStateShard pTextureState) { + this.textureState = pTextureState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setShaderState(RenderStateShard.ShaderStateShard pShaderState) { + this.shaderState = pShaderState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setTransparencyState(RenderStateShard.TransparencyStateShard pTransparencyState) { + this.transparencyState = pTransparencyState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setDepthTestState(RenderStateShard.DepthTestStateShard pDepthTestState) { + this.depthTestState = pDepthTestState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setCullState(RenderStateShard.CullStateShard pCullState) { + this.cullState = pCullState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setLightmapState(RenderStateShard.LightmapStateShard pLightmapState) { + this.lightmapState = pLightmapState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setOverlayState(RenderStateShard.OverlayStateShard pOverlayState) { + this.overlayState = pOverlayState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setLayeringState(RenderStateShard.LayeringStateShard pLayerState) { + this.layeringState = pLayerState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setOutputState(RenderStateShard.OutputStateShard pOutputState) { + this.outputState = pOutputState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setTexturingState(RenderStateShard.TexturingStateShard pTexturingState) { + this.texturingState = pTexturingState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setWriteMaskState(RenderStateShard.WriteMaskStateShard pWriteMaskState) { + this.writeMaskState = pWriteMaskState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setLineState(RenderStateShard.LineStateShard pLineState) { + this.lineState = pLineState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState.MutableCompositeStateBuilder setColorLogicState(RenderStateShard.ColorLogicStateShard pColorLogicState) { + this.colorLogicState = pColorLogicState; + return this; + } + + public EpicFightRenderTypes.MutableCompositeState createCompositeState(boolean pOutline) { + return this.createCompositeState(pOutline ? RenderType.OutlineProperty.AFFECTS_OUTLINE : RenderType.OutlineProperty.NONE); + } + + public EpicFightRenderTypes.MutableCompositeState createCompositeState(RenderType.OutlineProperty pOutlineState) { + return new EpicFightRenderTypes.MutableCompositeState( + this.textureState, + this.shaderState, + this.transparencyState, + this.depthTestState, + this.cullState, + this.lightmapState, + this.overlayState, + this.layeringState, + this.outputState, + this.texturingState, + this.writeMaskState, + this.lineState, + this.colorLogicState, + pOutlineState + ); + } + } + } + + private static final RenderType ENTITY_UI_COLORED = + create( + EpicFightMod.prefix("ui_color") + , DefaultVertexFormat.POSITION_COLOR + , VertexFormat.Mode.QUADS + , 256 + , true + , false + , RenderType.CompositeState.builder() + .setShaderState(POSITION_COLOR_SHADER) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setLightmapState(NO_LIGHTMAP) + .setOverlayState(NO_OVERLAY) + .createCompositeState(false) + ); + + private static final Function ENTITY_UI_TEXTURE = Util.memoize( + (textureLocation) -> create( + EpicFightMod.prefix("ui_texture") + , DefaultVertexFormat.POSITION_TEX + , VertexFormat.Mode.QUADS + , 256 + , true + , false + , RenderType.CompositeState.builder() + .setShaderState(POSITION_TEX_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(textureLocation, false, false)) + .setTransparencyState(NO_TRANSPARENCY) + .setLightmapState(NO_LIGHTMAP) + .setOverlayState(NO_OVERLAY) + .createCompositeState(false) + ) + ); + + private static final RenderType OBB = create( + EpicFightMod.prefix("debug_collider") + , DefaultVertexFormat.POSITION_COLOR_NORMAL + , VertexFormat.Mode.LINE_STRIP + , 256 + , false + , false + , RenderType.CompositeState.builder() + .setShaderState(POSITION_COLOR_SHADER) + .setLineState(new RenderStateShard.LineStateShard(OptionalDouble.empty())) + .setLayeringState(VIEW_OFFSET_Z_LAYERING) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setOutputState(ITEM_ENTITY_TARGET) + .setWriteMaskState(COLOR_DEPTH_WRITE) + .setCullState(NO_CULL) + .createCompositeState(false) + ); + + private static final RenderType DEBUG_QUADS = create( + EpicFightMod.prefix("debug_quad") + , DefaultVertexFormat.POSITION_COLOR + , VertexFormat.Mode.QUADS + , 256 + , false + , false + , RenderType.CompositeState.builder() + .setShaderState(POSITION_COLOR_SHADER) + .setLayeringState(VIEW_OFFSET_Z_LAYERING) + .setTransparencyState(NO_TRANSPARENCY) + .setWriteMaskState(COLOR_DEPTH_WRITE) + .setCullState(NO_CULL) + .createCompositeState(false) + ); + + private static final RenderType GUI_TRIANGLE = create( + EpicFightMod.prefix("gui_triangle") + , DefaultVertexFormat.POSITION_COLOR + , VertexFormat.Mode.TRIANGLES + , 256 + , false + , false + , RenderType.CompositeState.builder() + .setShaderState(RENDERTYPE_GUI_SHADER) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setDepthTestState(LEQUAL_DEPTH_TEST) + .createCompositeState(false) + ); + + private static final Function OVERLAY_MODEL = Util.memoize(texLocation -> { + return create( + EpicFightMod.prefix("overlay_model"), + DefaultVertexFormat.NEW_ENTITY, + VertexFormat.Mode.TRIANGLES, + 256, + false, + false, + RenderType.CompositeState.builder() + .setShaderState(RENDERTYPE_ENTITY_TRANSLUCENT_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(texLocation, false, false)) + .setWriteMaskState(COLOR_WRITE) + .setCullState(NO_CULL) + .setDepthTestState(EQUAL_DEPTH_TEST) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setLightmapState(LIGHTMAP) + .createCompositeState(false) + ); + } + ); + + private static final RenderType ENTITY_AFTERIMAGE_WHITE = + create( + EpicFightMod.prefix("entity_afterimage"), + DefaultVertexFormat.PARTICLE, + VertexFormat.Mode.TRIANGLES, + 256, + true, + true, + RenderType.CompositeState.builder() + .setShaderState(PARTICLE_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(EpicFightMod.identifier("textures/common/white.png"), false, false)) + .setCullState(NO_CULL) + .setWriteMaskState(COLOR_WRITE) + .setDepthTestState(EQUAL_DEPTH_TEST) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setLightmapState(LIGHTMAP) + .createCompositeState(false) + ); + + private static final RenderType ITEM_AFTERIMAGE_WHITE = + create( + EpicFightMod.prefix("item_afterimage"), + DefaultVertexFormat.PARTICLE, + VertexFormat.Mode.QUADS, + 256, + true, + true, + RenderType.CompositeState.builder() + .setShaderState(PARTICLE_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(EpicFightMod.identifier("textures/common/white.png"), false, false)) + .setCullState(NO_CULL) + .setWriteMaskState(COLOR_WRITE) + .setDepthTestState(EQUAL_DEPTH_TEST) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setLightmapState(LIGHTMAP) + .createCompositeState(false) + ); + + private static final Function ENTITY_PARTICLE = Util.memoize(texLocation -> { + return create( + EpicFightMod.prefix("entity_particle"), + DefaultVertexFormat.NEW_ENTITY, + VertexFormat.Mode.TRIANGLES, + 256, + true, + true, + RenderType.CompositeState.builder() + .setShaderState(RENDERTYPE_ENTITY_TRANSLUCENT_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(texLocation, false, false)) + .setWriteMaskState(COLOR_WRITE) + .setDepthTestState(EQUAL_DEPTH_TEST) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setCullState(NO_CULL) + .setLightmapState(LIGHTMAP) + .createCompositeState(false) + ); + }); + + private static final RenderType ITEM_PARTICLE = + create( + EpicFightMod.prefix("item_particle"), + DefaultVertexFormat.NEW_ENTITY, + VertexFormat.Mode.QUADS, + 256, + true, + true, + RenderType.CompositeState.builder() + .setShaderState(RENDERTYPE_ENTITY_TRANSLUCENT_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(InventoryMenu.BLOCK_ATLAS, false, false)) + .setWriteMaskState(COLOR_WRITE) + .setDepthTestState(EQUAL_DEPTH_TEST) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setCullState(NO_CULL) + .setLightmapState(LIGHTMAP) + .createCompositeState(false) + ); + + private static final Function ENTITY_PARTICLE_STENCIL = Util.memoize(texLocation -> { + return create( + EpicFightMod.prefix("entity_particle_stencil"), + DefaultVertexFormat.POSITION_TEX, + VertexFormat.Mode.TRIANGLES, + 256, + false, + false, + RenderType.CompositeState.builder() + .setShaderState(POSITION_TEX_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(texLocation, false, false)) + .setWriteMaskState(DEPTH_WRITE) + .createCompositeState(false) + ); + }); + + private static final RenderType ITEM_PARTICLE_STENCIL = + create( + EpicFightMod.prefix("item_particle_stencil"), + DefaultVertexFormat.POSITION_TEX, + VertexFormat.Mode.QUADS, + 256, + false, + false, + RenderType.CompositeState.builder() + .setShaderState(POSITION_TEX_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(InventoryMenu.BLOCK_ATLAS, false, false)) + .setWriteMaskState(DEPTH_WRITE) + .createCompositeState(false) + ); + + private static final RenderType.CompositeRenderType BLOCK_HIGHLIGHT = + create( + EpicFightMod.prefix("block_highlight"), + DefaultVertexFormat.BLOCK, + VertexFormat.Mode.QUADS, + 256, + false, + true, + RenderType.CompositeState.builder() + .setTextureState(new RenderStateShard.TextureStateShard(EpicFightMod.identifier("textures/common/white.png"), false, false)) + .setLightmapState(LIGHTMAP) + .setShaderState(RENDERTYPE_TRANSLUCENT_SHADER) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setDepthTestState(EQUAL_DEPTH_TEST) + //.setDepthTestState(NO_DEPTH_TEST) + .createCompositeState(false) + ); + + private static RenderType replaceTextureShard(ResourceLocation texToReplace, RenderType renderType) { + if (renderType instanceof CompositeRenderType compositeRenderType && compositeRenderType.state.textureState instanceof TextureStateShard texStateShard) { + CompositeState textureReplacedState = new CompositeState( + new RenderStateShard.TextureStateShard(texToReplace, texStateShard.blur, texStateShard.mipmap) + , compositeRenderType.state.shaderState + , compositeRenderType.state.transparencyState + , compositeRenderType.state.depthTestState + , compositeRenderType.state.cullState + , compositeRenderType.state.lightmapState + , compositeRenderType.state.overlayState + , compositeRenderType.state.layeringState + , compositeRenderType.state.outputState + , compositeRenderType.state.texturingState + , compositeRenderType.state.writeMaskState + , compositeRenderType.state.lineState + , compositeRenderType.state.colorLogicState + , compositeRenderType.state.outlineProperty + ); + + return new CompositeRenderType(renderType.name, renderType.format, compositeRenderType.mode(), renderType.bufferSize(), renderType.affectsCrumbling(), renderType.sortOnUpload, textureReplacedState); + } else { + return null; + } + } + + public static RenderType replaceTexture(ResourceLocation texLocation, RenderType renderType) { + if (TRIANGLED_RENDERTYPES_BY_NAME_TEXTURE.containsKey(renderType.name)) { + Map renderTypesByTexture = TRIANGLED_RENDERTYPES_BY_NAME_TEXTURE.get(renderType.name); + + if (renderTypesByTexture.containsKey(texLocation)) { + return renderTypesByTexture.get(texLocation); + } + } + + RenderType textureReplacedRenderType = replaceTextureShard(texLocation, renderType); + + if (textureReplacedRenderType == null) { + return renderType; + } + + Map renderTypesByTexture = TRIANGLED_RENDERTYPES_BY_NAME_TEXTURE.computeIfAbsent(textureReplacedRenderType.name, k -> Maps.newHashMap()); + renderTypesByTexture.put(texLocation, textureReplacedRenderType); + + return textureReplacedRenderType; + } + + public static RenderType entityUIColor() { + return ENTITY_UI_COLORED; + } + + public static RenderType entityUITexture(ResourceLocation resourcelocation) { + return ENTITY_UI_TEXTURE.apply(resourcelocation); + } + + public static RenderType debugCollider() { + return OBB; + } + + public static RenderType debugQuads() { + return DEBUG_QUADS; + } + + public static RenderType guiTriangle() { + return GUI_TRIANGLE; + } + + public static RenderType overlayModel(ResourceLocation textureLocation) { + return OVERLAY_MODEL.apply(textureLocation); + } + + public static RenderType entityAfterimageStencil(ResourceLocation textureLocation) { + return ENTITY_PARTICLE_STENCIL.apply(textureLocation); + } + + public static RenderType itemAfterimageStencil() { + return ITEM_PARTICLE_STENCIL; + } + + public static RenderType entityAfterimageTranslucent(ResourceLocation textureLocation) { + return ENTITY_PARTICLE.apply(textureLocation); + } + + public static RenderType itemAfterimageTranslucent() { + return ITEM_PARTICLE; + } + + public static RenderType entityAfterimageWhite() { + return ENTITY_AFTERIMAGE_WHITE; + } + + public static RenderType itemAfterimageWhite() { + return ITEM_AFTERIMAGE_WHITE; + } + + public static RenderType blockHighlight() { + return BLOCK_HIGHLIGHT; + } + + private static final Map WORLD_RENDERTYPES_COLORED_GLINT = new HashMap<> (); + + public static void freeUnusedWorldRenderTypes() { + WORLD_RENDERTYPES_COLORED_GLINT.entrySet().removeIf(entry -> entry.getKey().isRemoved()); + } + + public static void clearWorldRenderTypes() { + WORLD_RENDERTYPES_COLORED_GLINT.clear(); + } + + public static RenderType coloredGlintWorldRendertype(Entity owner, float r, float g, float b) { + CompositeRenderType glintRenderType = WORLD_RENDERTYPES_COLORED_GLINT.computeIfAbsent( + owner, + k -> create( + EpicFightMod.prefix("colored_glint"), + DefaultVertexFormat.POSITION_TEX, + VertexFormat.Mode.TRIANGLES, + 256, + false, + false, + EpicFightRenderTypes.MutableCompositeState.mutableStateBuilder() + .setShaderState(RENDERTYPE_ARMOR_ENTITY_GLINT_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(EpicFightMod.identifier("textures/entity/overlay/glint_white.png"), true, false)) + .setWriteMaskState(COLOR_WRITE) + .setCullState(NO_CULL) + .setDepthTestState(EQUAL_DEPTH_TEST) + .setTransparencyState(GLINT_TRANSPARENCY) + .setTexturingState(ENTITY_GLINT_TEXTURING) + .createCompositeState(false) + )); + + ((MutableCompositeState)glintRenderType.state).setShaderColor(r, g, b, 1.0F); + + return glintRenderType; + } + + public static RenderType coloredGlintWorldRendertype(Entity owner, int r, int g, int b) { + return coloredGlintWorldRendertype(owner, r / 255.0F, g / 255.0F, b / 255.0F); + } + + //Util class + private EpicFightRenderTypes() { + super(null, null, null, -1, false, false, null, null); + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/util/ParseUtil.java b/src/main/java/com/tiedup/remake/rig/util/ParseUtil.java new file mode 100644 index 0000000..fae2c45 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/util/ParseUtil.java @@ -0,0 +1,372 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import com.google.common.collect.Lists; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.JsonOps; + +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import it.unimi.dsi.fastutil.floats.FloatArrayList; +import it.unimi.dsi.fastutil.floats.FloatList; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.TagParser; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.registries.IForgeRegistry; +import com.tiedup.remake.rig.math.Vec3f; + +public class ParseUtil { + public static Integer[] toIntArray(JsonArray array) { + List result = Lists.newArrayList(); + + for (JsonElement je : array) { + result.add(je.getAsInt()); + } + + return result.toArray(new Integer[0]); + } + + public static Float[] toFloatArray(JsonArray array) { + List result = Lists.newArrayList(); + + for (JsonElement je : array) { + result.add(je.getAsFloat()); + } + + return result.toArray(new Float[0]); + } + + public static int[] toIntArrayPrimitive(JsonArray array) { + IntList result = new IntArrayList(); + + for (JsonElement je : array) { + result.add(je.getAsInt()); + } + + return result.toIntArray(); + } + + public static float[] toFloatArrayPrimitive(JsonArray array) { + FloatList result = new FloatArrayList(); + + for (JsonElement je : array) { + result.add(je.getAsFloat()); + } + + return result.toFloatArray(); + } + + public static int[] unwrapIntWrapperArray(Number[] wrapperArray) { + int[] iarray = new int[wrapperArray.length]; + + for (int i = 0; i < wrapperArray.length; i++) { + iarray[i] = (int)wrapperArray[i]; + } + + return iarray; + } + + public static float[] unwrapFloatWrapperArray(Number[] wrapperArray) { + float[] farray = new float[wrapperArray.length]; + + for (int i = 0; i < wrapperArray.length; i++) { + farray[i] = (float)wrapperArray[i]; + } + + return farray; + } + + public static JsonObject farrayToJsonObject(float[] array, int stride) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("stride", stride); + jsonObject.addProperty("count", array.length / stride); + JsonArray jsonArray = new JsonArray(); + + for (float element : array) { + jsonArray.add(element); + } + + jsonObject.add("array", jsonArray); + + return jsonObject; + } + + public static JsonObject iarrayToJsonObject(int[] array, int stride) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("stride", stride); + jsonObject.addProperty("count", array.length / stride); + JsonArray jsonArray = new JsonArray(); + + for (int element : array) { + jsonArray.add(element); + } + + jsonObject.add("array", jsonArray); + + return jsonObject; + } + + public static Vec3f toVector3f(JsonArray array) { + float[] result = toFloatArrayPrimitive(array); + + if (result.length < 3) { + throw new IllegalArgumentException("Requires more than 3 elements to convert into 3d vector."); + } + + return new Vec3f(result[0], result[1], result[2]); + } + + public static Vec3 toVector3d(JsonArray array) { + DoubleList result = new DoubleArrayList(); + + for (JsonElement je : array) { + result.add(je.getAsDouble()); + } + + if (result.size() < 3) { + throw new IllegalArgumentException("Requires more than 3 elements to convert into 3d vector."); + } + + return new Vec3(result.getDouble(0), result.getDouble(1), result.getDouble(2)); + } + + public static AttributeModifier toAttributeModifier(CompoundTag tag) { + AttributeModifier.Operation operation = AttributeModifier.Operation.valueOf(tag.getString("operation").toUpperCase(Locale.ROOT)); + + return new AttributeModifier(UUID.fromString(tag.getString("uuid")), tag.getString("name"), tag.getDouble("amount"), operation); + } + + public static String nullOrToString(T obj, Function toString) { + return obj == null ? "" : toString.apply(obj); + } + + public static V nullOrApply(T obj, Function apply) { + if (obj == null) { + return null; + } + + try { + return apply.apply(obj); + } catch (Exception e) { + return null; + } + } + + public static T nvl(T a, T b) { + return a == null ? b : a; + } + + public static String snakeToSpacedCamel(Object obj) { + if (obj == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + boolean upperNext = true; + String toStr = obj.toString().toLowerCase(Locale.ROOT); + + for (String sElement : toStr.split("")) { + if (upperNext) { + sElement = sElement.toUpperCase(Locale.ROOT); + upperNext = false; + } + + if ("_".equals(sElement)) { + upperNext = true; + sb.append(" "); + } else { + sb.append(sElement); + } + } + + return sb.toString(); + } + + public static boolean compareNullables(@Nullable Object obj1, @Nullable Object obj2) { + if (obj1 == null) { + if (obj2 == null) { + return true; + } else { + return false; + } + } else { + return obj1.equals(obj2); + } + } + + public static String nullParam(Object obj) { + return obj == null ? "" : obj.toString(); + } + + public static String getRegistryName(T obj, IForgeRegistry registry) { + return obj == null ? "" : registry.getKey(obj).toString(); + } + + public static T getOrSupply(CompoundTag compTag, String name, Supplier tag) { + return getOrDefaultTag(compTag, name, tag.get()); + } + + @SuppressWarnings("unchecked") + public static T getOrDefaultTag(CompoundTag compTag, String name, T tag) { + if (compTag.contains(name)) { + return (T)compTag.get(name); + } + + compTag.put(name, tag); + + return tag; + } + + public static boolean isParsableAllowingMinus(String s, Function parser) { + if ("-".equals(s)) { + return true; + } + + try { + parser.apply(s); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + public static boolean isParsable(String s, Function parser) { + try { + parser.apply(s); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + public static String valueOfOmittingType(T value) { + try { + return String.valueOf(value).replaceAll("[df]", ""); + } catch (NumberFormatException e) { + return null; + } + } + + public static T parseOrGet(String value, Function parseFunction, T defaultValue) { + try { + return parseFunction.apply(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + public static Set> mapEntryToPair(Set> entrySet) { + return entrySet.stream().map((entry) -> Pair.of(entry.getKey(), entry.getValue())).collect(Collectors.toSet()); + } + + public static List remove(Collection collection, T object) { + List copied = new ArrayList<> (collection); + copied.remove(object); + return copied; + } + + public static > T enumValueOfOrNull(Class enumCls, String enumName) { + try { + return Enum.valueOf(enumCls, enumName.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException | NullPointerException e) { + return null; + } + } + + public static JsonObject convertToJsonObject(CompoundTag compoundtag) { + JsonObject root = CompoundTag.CODEC.encodeStart(JsonOps.INSTANCE, compoundtag).get().left().get().getAsJsonObject(); + + for (Map.Entry entry : compoundtag.tags.entrySet()) { + if (entry.getValue() instanceof ByteTag byteTag && (byteTag.getAsByte() == 0 || byteTag.getAsByte() == 1)) { + root.remove(entry.getKey()); + root.addProperty(entry.getKey(), byteTag.getAsByte() == 1); + } + } + + return root; + } + + public static String toLowerCase(String s) { + return s.toLowerCase(Locale.ROOT); + } + + public static String toUpperCase(String s) { + return s.toUpperCase(Locale.ROOT); + } + + public static String getBytesSHA256Hash(byte[] bytes) { + String hashString = ""; + + try { + MessageDigest sh = MessageDigest.getInstance("SHA-256"); + sh.update(bytes); + byte byteData[] = sh.digest(); + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < byteData.length; i++) { + sb.append(Integer.toString((byteData[i] & 0xFF) + 0x100, 16).substring(1)); + } + + hashString = sb.toString(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + hashString = null; + } + + return hashString; + } + + public static int parseCharacterToNumber(char c) { + if (c < '0' || c > '9') { + throw new IllegalArgumentException(c + "is not a character represents number"); + } + + return c - '0'; + } + + public static T orElse(T value, Supplier defaultVal) { + Objects.requireNonNull(defaultVal); + + return value == null ? defaultVal.get() : value; + } + + public static CompoundTag parseTagOrThrow(JsonElement jsonElement) { + try { + return TagParser.parseTag(jsonElement.toString()); + } catch (CommandSyntaxException e) { + throw new RuntimeException("Can't parse element:", e); + } + } + + private ParseUtil() {} +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/util/datastruct/ClearableIdMapper.java b/src/main/java/com/tiedup/remake/rig/util/datastruct/ClearableIdMapper.java new file mode 100644 index 0000000..eec76ac --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/util/datastruct/ClearableIdMapper.java @@ -0,0 +1,25 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.util.datastruct; + +import net.minecraft.core.IdMapper; + +public class ClearableIdMapper extends IdMapper { + public ClearableIdMapper() { + super(512); + } + + public ClearableIdMapper(int size) { + super(size); + } + + public void clear() { + this.tToId.clear(); + this.idToT.clear(); + this.nextId = 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/tiedup/remake/rig/util/datastruct/ModifiablePair.java b/src/main/java/com/tiedup/remake/rig/util/datastruct/ModifiablePair.java new file mode 100644 index 0000000..3bbf145 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/util/datastruct/ModifiablePair.java @@ -0,0 +1,37 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.util.datastruct; + +public class ModifiablePair { + public static ModifiablePair of(F first, S second) { + return new ModifiablePair<>(first, second); + } + + private F first; + private S second; + + private ModifiablePair(F first, S second) { + this.first = first; + this.second = second; + } + + public F getFirst() { + return this.first; + } + + public S getSecond() { + return this.second; + } + + public void setFirst(F first) { + this.first = first; + } + + public void setSecond(S second) { + this.second = second; + } +} diff --git a/src/main/java/com/tiedup/remake/rig/util/datastruct/TypeFlexibleHashMap.java b/src/main/java/com/tiedup/remake/rig/util/datastruct/TypeFlexibleHashMap.java new file mode 100644 index 0000000..2699e26 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/util/datastruct/TypeFlexibleHashMap.java @@ -0,0 +1,43 @@ +/* + * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) + * by the Epic Fight Team, licensed under GPLv3. + * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.util.datastruct; + +import java.util.HashMap; + +import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap.TypeKey; + +@SuppressWarnings("serial") +public class TypeFlexibleHashMap> extends HashMap { + final boolean immutable; + + public TypeFlexibleHashMap(boolean immutable) { + this.immutable = immutable; + } + + @SuppressWarnings("unchecked") + public T put(TypeKey typeKey, T val) { + if (this.immutable) { + throw new UnsupportedOperationException(); + } + + return (T)super.put((A)typeKey, val); + } + + @SuppressWarnings("unchecked") + public T get(A typeKey) { + return (T)super.get(typeKey); + } + + @SuppressWarnings("unchecked") + public T getOrDefault(A typeKey) { + return (T)super.getOrDefault(typeKey, typeKey.defaultValue()); + } + + public interface TypeKey { + T defaultValue(); + } +} \ No newline at end of file