diff --git a/common/src/main/java/org/figuramc/figura/ducks/LivingEntityRendererAccessor.java b/common/src/main/java/org/figuramc/figura/ducks/LivingEntityRendererAccessor.java new file mode 100644 index 000000000..02b523269 --- /dev/null +++ b/common/src/main/java/org/figuramc/figura/ducks/LivingEntityRendererAccessor.java @@ -0,0 +1,8 @@ +package org.figuramc.figura.ducks; + +import java.util.OptionalInt; + +public class LivingEntityRendererAccessor { + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") // cry + public static OptionalInt overrideOverlay = OptionalInt.empty(); +} diff --git a/common/src/main/java/org/figuramc/figura/lua/FiguraAPIManager.java b/common/src/main/java/org/figuramc/figura/lua/FiguraAPIManager.java index dc80f8563..3e99e4514 100644 --- a/common/src/main/java/org/figuramc/figura/lua/FiguraAPIManager.java +++ b/common/src/main/java/org/figuramc/figura/lua/FiguraAPIManager.java @@ -87,6 +87,7 @@ public class FiguraAPIManager { add(RenderTask.class); add(ItemTask.class); add(BlockTask.class); + add(EntityTask.class); add(TextTask.class); add(SpriteTask.class); diff --git a/common/src/main/java/org/figuramc/figura/lua/docs/FiguraDocsManager.java b/common/src/main/java/org/figuramc/figura/lua/docs/FiguraDocsManager.java index 41dd917c1..30d3ad587 100644 --- a/common/src/main/java/org/figuramc/figura/lua/docs/FiguraDocsManager.java +++ b/common/src/main/java/org/figuramc/figura/lua/docs/FiguraDocsManager.java @@ -156,7 +156,8 @@ public class FiguraDocsManager { BlockTask.class, ItemTask.class, TextTask.class, - SpriteTask.class + SpriteTask.class, + EntityTask.class )); put("player", List.of( diff --git a/common/src/main/java/org/figuramc/figura/mixin/LivingEntityAccessor.java b/common/src/main/java/org/figuramc/figura/mixin/LivingEntityAccessor.java index 8e131e5fe..d63ed6211 100644 --- a/common/src/main/java/org/figuramc/figura/mixin/LivingEntityAccessor.java +++ b/common/src/main/java/org/figuramc/figura/mixin/LivingEntityAccessor.java @@ -15,4 +15,8 @@ public interface LivingEntityAccessor { @Intrinsic @Invoker("getCurrentSwingDuration") int getSwingDuration(); + + @Intrinsic + @Invoker("updateWalkAnimation") + void invokeUpdateWalkAnimation(float distance); } diff --git a/common/src/main/java/org/figuramc/figura/mixin/render/renderers/LivingEntityRendererMixin.java b/common/src/main/java/org/figuramc/figura/mixin/render/renderers/LivingEntityRendererMixin.java index 7a48d34f8..208c362a1 100644 --- a/common/src/main/java/org/figuramc/figura/mixin/render/renderers/LivingEntityRendererMixin.java +++ b/common/src/main/java/org/figuramc/figura/mixin/render/renderers/LivingEntityRendererMixin.java @@ -14,6 +14,7 @@ import org.figuramc.figura.avatar.Avatar; import org.figuramc.figura.avatar.AvatarManager; import org.figuramc.figura.config.Configs; +import org.figuramc.figura.ducks.LivingEntityRendererAccessor; import org.figuramc.figura.gui.PopupMenu; import org.figuramc.figura.lua.api.vanilla_model.VanillaPart; import org.figuramc.figura.math.matrix.FiguraMat4; @@ -28,6 +29,7 @@ import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @@ -62,6 +64,18 @@ private void onRender(T livingEntity, float f, float g, PoseStack poseStack, Mul lastPose = poseStack.last().pose(); } + @ModifyArg( + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/model/EntityModel;renderToBuffer(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;IIFFFF)V" + ), + method = "render(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", + index = 3 + ) + private int customOverlay(int thing) { + return LivingEntityRendererAccessor.overrideOverlay.orElse(thing); + } + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/client/model/EntityModel;setupAnim(Lnet/minecraft/world/entity/Entity;FFFFF)V", shift = At.Shift.AFTER), method = "render(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", cancellable = true) private void preRender(T entity, float yaw, float delta, PoseStack poseStack, MultiBufferSource bufferSource, int light, CallbackInfo ci) { if (currentAvatar == null) diff --git a/common/src/main/java/org/figuramc/figura/model/FiguraModelPart.java b/common/src/main/java/org/figuramc/figura/model/FiguraModelPart.java index cd6d41401..3495c873b 100644 --- a/common/src/main/java/org/figuramc/figura/model/FiguraModelPart.java +++ b/common/src/main/java/org/figuramc/figura/model/FiguraModelPart.java @@ -1,6 +1,5 @@ package org.figuramc.figura.model; -import com.mojang.datafixers.util.Pair; import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.texture.OverlayTexture; import org.figuramc.figura.avatar.Avatar; @@ -1300,6 +1299,19 @@ public SpriteTask newSprite(@LuaNotNil String name) { return task; } + @LuaWhitelist + @LuaMethodDoc( + overloads = @LuaMethodOverload( + argumentTypes = String.class, + argumentNames = "taskName" + ), + value = "model_part.new_entity") + public EntityTask newEntity(@LuaNotNil String name) { + EntityTask task = new EntityTask(name, owner, this); + this.renderTasks.put(name, task); + return task; + } + @LuaWhitelist @LuaMethodDoc(overloads = @LuaMethodOverload(argumentTypes = RenderTask.class, argumentNames = "renderTask"), value = "model_part.add_task") public RenderTask addTask(@LuaNotNil RenderTask renderTask) { diff --git a/common/src/main/java/org/figuramc/figura/model/rendertasks/EntityTask.java b/common/src/main/java/org/figuramc/figura/model/rendertasks/EntityTask.java new file mode 100644 index 000000000..7a1a15efd --- /dev/null +++ b/common/src/main/java/org/figuramc/figura/model/rendertasks/EntityTask.java @@ -0,0 +1,166 @@ +package org.figuramc.figura.model.rendertasks; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import org.figuramc.figura.avatar.Avatar; +import org.figuramc.figura.ducks.LivingEntityRendererAccessor; +import org.figuramc.figura.lua.LuaWhitelist; +import org.figuramc.figura.lua.api.entity.EntityAPI; +import org.figuramc.figura.lua.docs.LuaMethodDoc; +import org.figuramc.figura.lua.docs.LuaMethodOverload; +import org.figuramc.figura.lua.docs.LuaTypeDoc; +import org.figuramc.figura.math.vector.FiguraVec2; +import org.figuramc.figura.mixin.LivingEntityAccessor; +import org.figuramc.figura.model.FiguraModelPart; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.luaj.vm2.LuaError; + +import java.util.OptionalInt; +import java.util.function.Function; + +@LuaWhitelist +@LuaTypeDoc( + name = "EntityTask", + value = "entity_task" +) +public class EntityTask extends RenderTask { + + @Nullable Entity entity; + long ticksSinceEntity; + + public EntityTask(String name, Avatar owner, FiguraModelPart parent) { + super(name, owner, parent); + } + + @Override + public void render(PoseStack stack, MultiBufferSource buffer, int light, int overlay) { + stack.scale(16, 16, 16); + + if (entity != null) { + assert Minecraft.getInstance().level != null; + entity.tickCount = (int) (Minecraft.getInstance().level.getGameTime() - ticksSinceEntity); + EntityRenderDispatcher dispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + boolean h = dispatcher.shouldRenderHitBoxes(); + dispatcher.setRenderHitBoxes(false); + OptionalInt prev = LivingEntityRendererAccessor.overrideOverlay; + LivingEntityRendererAccessor.overrideOverlay = OptionalInt.of(this.customization.overlay != null ? this.customization.overlay : overlay); + try { + Minecraft.getInstance().getEntityRenderDispatcher() + .render( + entity, 0.0, 0.0, 0.0, 0.0F, Minecraft.getInstance().getFrameTime(), stack, buffer, + this.customization.light != null ? this.customization.light : light + ); + } + finally { + LivingEntityRendererAccessor.overrideOverlay = prev; + dispatcher.setRenderHitBoxes(h); + } + } + } + + + @Override + public int getComplexity() { + return 20; // good enough + } + + @Override + public boolean shouldRender() { + return super.shouldRender() && entity != null; + } + + @LuaWhitelist + @LuaMethodDoc("entity_task.as_entity") + public EntityAPI asEntity() { + return entity == null ? null : new EntityAPI<>(entity); + } + + @LuaWhitelist + @Contract("_,_->this") + @LuaMethodDoc( + overloads = { + @LuaMethodOverload( + argumentTypes = String.class, + argumentNames = "nbt" + ), + @LuaMethodOverload( + argumentTypes = {String.class, String.class}, + argumentNames = {"id", "nbt"} + ) + }, + + value = "entity_task.set_nbt" + ) + public EntityTask setNbt(String nbtOrId, String nullOrNbt) { + try { + CompoundTag finalNbt; + if(nullOrNbt == null) { + finalNbt = (new TagParser(new StringReader(nbtOrId))).readStruct(); + + if (!finalNbt.contains("id", CompoundTag.TAG_STRING)) { + throw new LuaError("Nbt must contain id"); + } + } + else { + finalNbt = (new TagParser(new StringReader(nullOrNbt))).readStruct(); + finalNbt.put("id", StringTag.valueOf(nbtOrId)); + } + + assert Minecraft.getInstance().level != null; + entity = EntityType.loadEntityRecursive(finalNbt, Minecraft.getInstance().level, Function.identity()); + if (entity == null) { + throw new LuaError("Could not create entity"); + } + ticksSinceEntity = Minecraft.getInstance().level.getGameTime() - entity.tickCount; + } catch (CommandSyntaxException e) { + throw new LuaError(e.getMessage()); + } + return this; + } + + @LuaWhitelist + @Contract("_->this") + @LuaMethodDoc( + overloads = @LuaMethodOverload( + argumentTypes = String.class, + argumentNames = "distance" + ), + value = "entity_task.update_walking_distance" + ) + public EntityTask updateWalkingDistance(float distance) { + if(entity != null && entity instanceof LivingEntity living) { + ((LivingEntityAccessor) living).invokeUpdateWalkAnimation(distance); + } + return this; + } + + @LuaWhitelist + @Contract("_->this") + @LuaMethodDoc( + overloads = @LuaMethodOverload( + argumentTypes = FiguraVec2.class, + argumentNames = "rotation" + ), + value = "entity_task.set_head_rotation" + ) + public EntityTask setHeadRotation(FiguraVec2 vec2) { + if(entity != null && entity instanceof LivingEntity living) { + living.yHeadRot = (float) vec2.y; + living.yHeadRotO = (float) vec2.y; + living.setXRot((float) vec2.x); + living.xRotO = (float) vec2.x; + } + return this; + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/figura/lang/en_us.json b/common/src/main/resources/assets/figura/lang/en_us.json index b6a35b3e5..aed85254e 100644 --- a/common/src/main/resources/assets/figura/lang/en_us.json +++ b/common/src/main/resources/assets/figura/lang/en_us.json @@ -1277,6 +1277,7 @@ "figura.docs.model_part.new_item": "Adds a new Item Render Task on this part", "figura.docs.model_part.new_block": "Adds a new Block Render Task on this part", "figura.docs.model_part.new_sprite": "Adds a new Sprite Render Task on this part", + "figura.docs.model_part.new_entity": "Adds a new Entity Render Task on this part", "figura.docs.model_part.add_task": "Adds the given Render Task on this part", "figura.docs.model_part.get_task": "Gets the Render Task with the given name from this part\nReturns a table with all tasks if a name is not given", "figura.docs.model_part.remove_task": "Removes the Task with the given name from this part\nRemoves ALL tasks if a name is not given", @@ -1413,6 +1414,11 @@ "figura.docs.sprite_task.get_render_type": "Gets the name of the current render type for this sprite", "figura.docs.sprite_task.set_render_type": "Sets the current render type of this sprite\nTRANSLUCENT by default\nCheck the docs enum command for all render types", "figura.docs.sprite_task.get_vertices": "Returns a table with all 4 vertices of this sprite\nChanging the values through other functions will reset those vertices", + "figura.docs.entity_task": "A task for rendering an Entity", + "figura.docs.entity_task.as_entity": "Returns the entity associated with this task, or nil if the entity could not exist for any reason.\nDue to the special circumstances some readings of the subsequent value may be completely useless", + "figura.docs.entity_task.set_nbt": "Sets [the nbt of] the entity", + "figura.docs.entity_task.update_walking_distance": "Updates the walking animations given the new information, if applicable. For an expected result it should be called every tick with the appropriate value", + "figura.docs.entity_task.set_head_rotation": "Updates the head rotation of the entity, if applicable", "figura.docs.renderer": "A global API providing functions that change the way Minecraft renders your player", "figura.docs.renderer.render_fire": "Whether or not you should visually have the fire effect while on fire\nTrue by default", "figura.docs.renderer.render_vehicle": "Whether or not your vehicle (boat, minecart, horse, whatever) will be rendered\nTrue by default",