Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial animation setup #35

Open
wants to merge 2 commits into
base: multi/1.21.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions common/src/main/java/com/mrbysco/armorposer/Reference.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ public class Reference {
public static final String MOD_NAME = "Armor Poser";
public static final Logger LOGGER = LogUtils.getLogger();

public static final int ANIMATION_SEARCH_RADIUS = 32;

public static final ResourceLocation SYNC_PACKET_ID = ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "sync_packet");
public static final ResourceLocation SWAP_PACKET_ID = ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "swap_packet");
public static final ResourceLocation RENAME_PACKET_ID = ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "rename_packet");
public static final ResourceLocation SCREEN_PACKET_ID = ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "screen_packet");
public static final ResourceLocation COPY_TO_BOOK_PACKET_ID = ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "copy_to_book");

public static final Map<String, String> defaultPoseMap = initializePoseMap();

Expand Down Expand Up @@ -83,4 +85,9 @@ public static boolean canResize(Player player) {
}
return true;
}

public static boolean animationEnabled = false;
public static void setAnimationEnabled(boolean value) {
animationEnabled = value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.mrbysco.armorposer.animation;

import com.mrbysco.armorposer.Reference;
import com.mrbysco.armorposer.data.BookCopyData;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.targeting.TargetingConditions;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.entity.decoration.ItemFrame;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.WrittenBookContent;
import net.minecraft.world.phys.AABB;

import java.util.HashSet;
import java.util.Set;

public class AnimationHandler {
// Cache of item frames positions that are currently being animated
private static final Set<BlockPos> cachedFrames = new HashSet<>();

/**
* Handles the animation for the armor stand when an item frame is powered while holding a compatible armor poser book
*
* @param frame The item frame that is being checked
*/
public static void onFrameUpdate(ItemFrame frame) {
if (frame.level() instanceof ServerLevel serverLevel && frame.getDirection() == Direction.UP) {
BlockPos pos = frame.blockPosition();
ItemStack frameStack = frame.getItem();
// Check if the item frame is holding a valid armor poser book
if (isValidArmorPoserBook(frameStack)) {
// Check if the item frame is powered
if (serverLevel.getSignal(pos, Direction.UP) >= 1) {
if (!cachedFrames.contains(pos)) {
cachedFrames.add(pos);
WrittenBookContent bookContent = frameStack.getOrDefault(DataComponents.WRITTEN_BOOK_CONTENT, WrittenBookContent.EMPTY);
// The name to match the armor stand with (empty string if no match required)
String match = "";
if (!bookContent.pages().isEmpty()) {
var firstPage = bookContent.pages().getFirst();
if (!firstPage.raw().getString().isEmpty())
match = firstPage.raw().getString();
}
// Targeting conditions for the armor stand
TargetingConditions conditions = TargetingConditions.forNonCombat();
if (!match.isEmpty()) {
String finalMatch = match;
conditions.selector((livingEntity, level) ->
livingEntity.getName().getString().equals(finalMatch));
}
// Search for the nearest armor stand within the search radius
var nearestStand = serverLevel.getNearestEntity(ArmorStand.class, conditions, null, pos.getX(), pos.getY(), pos.getZ(),
AABB.ofSize(frame.position(), Reference.ANIMATION_SEARCH_RADIUS, Reference.ANIMATION_SEARCH_RADIUS, Reference.ANIMATION_SEARCH_RADIUS));
if (nearestStand != null) {
CompoundTag customTag = frameStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag();
// Check if the book contains a saved pose
if (customTag.contains("SavedPose")) {
CompoundTag poseTag = customTag.getCompound("SavedPose");
BookCopyData bookCopyData = new BookCopyData(nearestStand.getUUID(), poseTag);
bookCopyData.handleFrame(nearestStand);
}
}
}
} else {
cachedFrames.remove(pos);
}
}
}
}

/**
* Checks if the item frame is holding a valid armor poser book
*
* @param stack The item stack that is being checked
* @return True if the item stack is a valid armor poser book
*/
private static boolean isValidArmorPoserBook(ItemStack stack) {
return stack.is(Items.WRITTEN_BOOK)
&& stack.getCustomName() != null
&& stack.getCustomName().getString().equals("Armor Poser")
&& stack.has(DataComponents.CUSTOM_DATA);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,13 @@ public void init() {
.tooltip(Tooltip.create(Component.translatable("armorposer.gui.tooltip.poses"))).build());
this.addRenderableWidget(Button.builder(Component.translatable("armorposer.gui.label.copy"), (button) -> {
CompoundTag compound = this.writeFieldsToNBT();
String clipboardData = compound.toString();
if (this.minecraft != null) {
this.minecraft.keyboardHandler.setClipboard(clipboardData);
if (hasShiftDown()) {
Services.PLATFORM.copyArmorStandPose(this.entityArmorStand, compound);
} else {
String clipboardData = compound.toString();
if (this.minecraft != null) {
this.minecraft.keyboardHandler.setClipboard(clipboardData);
}
}
}).bounds(offsetX, offsetY + 22, 42, 20).tooltip(Tooltip.create(Component.translatable("armorposer.gui.tooltip.copy"))).build());
this.addRenderableWidget(Button.builder(Component.translatable("armorposer.gui.label.paste"), (button) -> {
Expand Down
82 changes: 82 additions & 0 deletions common/src/main/java/com/mrbysco/armorposer/data/BookCopyData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.mrbysco.armorposer.data;

import net.minecraft.core.UUIDUtil;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomData;

import java.util.UUID;

public record BookCopyData(UUID entityUUID, CompoundTag tag) {
public static final StreamCodec<FriendlyByteBuf, BookCopyData> STREAM_CODEC = StreamCodec.composite(
UUIDUtil.STREAM_CODEC,
BookCopyData::entityUUID,
ByteBufCodecs.COMPOUND_TAG,
BookCopyData::tag,
BookCopyData::new);

/**
* Handles applying the pose data to the player's offhand book if it's an armor poser book
*
* @param player The player to apply the data to
*/
public void handleData(Player player) {
if (!tag.isEmpty()) {
ItemStack offStack = player.getOffhandItem();
// Check if the player is holding an armor poser book
if (offStack.is(Items.WRITTEN_BOOK) && offStack.getCustomName() != null && offStack.getCustomName().getString().equals("Armor Poser")) {
CustomData data = offStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
CompoundTag tagCopy = data.copyTag();
// Set the datapack to ArmorStatuesV2 to make it compatible with the Armor Statues Datapack
tagCopy.putString("datapack", "ArmorStatuesV2");
// Set the pose data to the book
tagCopy.put("SavedPose", tag);
offStack.set(DataComponents.CUSTOM_DATA, CustomData.of(tagCopy));
}
}
}

/**
* Handles the pose data for the armor stand
*
* @param armorStand The armor stand to apply the pose to
*/
public void handleFrame(ArmorStand armorStand) {
CompoundTag entityTag = armorStand.saveWithoutId(new CompoundTag());
CompoundTag entityTagCopy = entityTag.copy();

if (!tag.isEmpty()) {
entityTagCopy.merge(tag);
armorStand.load(entityTagCopy);
armorStand.setUUID(entityUUID);

ListTag tagList = tag.getList("Move", Tag.TAG_DOUBLE);
double xOffset = tagList.getDouble(0);
double yOffset = tagList.getDouble(1);
double zOffset = tagList.getDouble(2);
if (xOffset != 0 || yOffset != 0 || zOffset != 0)
armorStand.setPosRaw(armorStand.getX() + xOffset,
armorStand.getY() + yOffset,
armorStand.getZ() + zOffset);

double scale = tag.getDouble("Scale");
if (scale > 0) {
AttributeInstance attributeInstance = armorStand.getAttributes().getInstance(Attributes.SCALE);
if (attributeInstance != null) {
attributeInstance.setBaseValue(scale);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.mrbysco.armorposer.packets;

import com.mrbysco.armorposer.Reference;
import com.mrbysco.armorposer.data.BookCopyData;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;

public record ArmorStandCopyToBookPayload(BookCopyData data) implements CustomPacketPayload {
public static final StreamCodec<FriendlyByteBuf, ArmorStandCopyToBookPayload> CODEC = CustomPacketPayload.codec(
ArmorStandCopyToBookPayload::write,
ArmorStandCopyToBookPayload::new);
public static final Type<ArmorStandCopyToBookPayload> ID = new Type<>(Reference.COPY_TO_BOOK_PACKET_ID);

public ArmorStandCopyToBookPayload(final FriendlyByteBuf packetBuffer) {
this(BookCopyData.STREAM_CODEC.decode(packetBuffer));
}

public void write(FriendlyByteBuf buf) {
BookCopyData.STREAM_CODEC.encode(buf, data());
}

@Override
public Type<? extends CustomPacketPayload> type() {
return ID;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public interface IPlatformHelper {
*/
void renameArmorStand(ArmorStand armorStand, String newName);

/**
* Copy Armor Stand pose to book
*/
void copyArmorStandPose(ArmorStand armorStand, CompoundTag compound);

/**
* Allow scrolling to increase/decrease the angle of text fields
*/
Expand Down
4 changes: 4 additions & 0 deletions common/src/main/resources/assets/armorposer/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"armorposer.config.resizeWhitelist.tooltip": "List of players that are allowed to resize the Armor Stand when restrictResizeToOP is enabled",
"armorposer.config.restrictResizeToOP": "Restrict Resize To OP",
"armorposer.config.restrictResizeToOP.tooltip": "Restrict the ability to resize the Armor Stand to server operators",
"armorposer.config.enableAnimation": "Enable Animation",
"armorposer.config.enableAnimation.tooltip": "Restrict the ability to resize the Armor Stand to server operators",
"armorposer.configuration.title": "Armor Poser",
"armorposer.gui.armor_list.list": "Armor Stands",
"armorposer.gui.armor_list.locate": "Locate",
Expand Down Expand Up @@ -120,5 +122,7 @@
"text.autoconfig.armorposer.option.general.resizeWhitelist.@Tooltip": "List of players that are allowed to resize the Armor Stand when restrictResizeToOP is enabled",
"text.autoconfig.armorposer.option.general.restrictResizeToOP": "Restrict Resize To OP",
"text.autoconfig.armorposer.option.general.restrictResizeToOP.@Tooltip": "Restrict the ability to resize the Armor Stand to server operators",
"text.autoconfig.armorposer.option.general.enableAnimation": "Enable Animation",
"text.autoconfig.armorposer.option.general.enableAnimation.@Tooltip": "Restrict the ability to resize the Armor Stand to server operators",
"text.autoconfig.armorposer.title": "Armor Poser"
}
23 changes: 23 additions & 0 deletions fabric/src/main/java/com/mrbysco/armorposer/ArmorPoser.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.mrbysco.armorposer;

import com.mrbysco.armorposer.config.PoserConfig;
import com.mrbysco.armorposer.data.BookCopyData;
import com.mrbysco.armorposer.data.RenameData;
import com.mrbysco.armorposer.data.SwapData;
import com.mrbysco.armorposer.data.SyncData;
import com.mrbysco.armorposer.handlers.EventHandler;
import com.mrbysco.armorposer.packets.ArmorStandCopyToBookPayload;
import com.mrbysco.armorposer.packets.ArmorStandRenamePayload;
import com.mrbysco.armorposer.packets.ArmorStandScreenPayload;
import com.mrbysco.armorposer.packets.ArmorStandSwapPayload;
Expand All @@ -13,10 +15,12 @@
import me.shedaniel.autoconfig.ConfigHolder;
import me.shedaniel.autoconfig.serializer.Toml4jConfigSerializer;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.player.UseItemCallback;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.decoration.ArmorStand;

Expand All @@ -26,6 +30,18 @@ public class ArmorPoser implements ModInitializer {
@Override
public void onInitialize() {
config = AutoConfig.register(PoserConfig.class, Toml4jConfigSerializer::new);
config.registerLoadListener((holder, config) -> {
Reference.setAnimationEnabled(config.general.enableAnimation);
return InteractionResult.PASS;
});
config.registerSaveListener((holder, config) -> {
Reference.setAnimationEnabled(config.general.enableAnimation);
return InteractionResult.PASS;
});

ServerLifecycleEvents.SERVER_STARTING.register((server) -> {
Reference.setAnimationEnabled(config.get().general.enableAnimation);
});

UseItemCallback.EVENT.register((player, world, hand) -> EventHandler.onPlayerRightClickItem(player, hand));

Expand Down Expand Up @@ -67,5 +83,12 @@ public void onInitialize() {
}
});
});
PayloadTypeRegistry.playC2S().register(ArmorStandCopyToBookPayload.ID, ArmorStandCopyToBookPayload.CODEC);
ServerPlayNetworking.registerGlobalReceiver(ArmorStandCopyToBookPayload.ID, (payload, context) -> {
BookCopyData bookData = payload.data();
context.player().server.execute(() -> {
bookData.handleData(context.player());
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public static class General {
@ConfigEntry.Gui.Tooltip
@Comment("Allow scrolling to add / decrease an angle value in the posing screen")
public boolean allowScrolling = true;
@Comment("Enable Armor Poser's animation system for the Armor Stand")
public boolean enableAnimation = false;
@ConfigEntry.Gui.Tooltip
@Comment("Restrict the ability to resize the Armor Stand to server operators")
public boolean restrictResizeToOP = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.mrbysco.armorposer.handlers;

import com.mrbysco.armorposer.ArmorPoser;
import com.mrbysco.armorposer.Reference;
import com.mrbysco.armorposer.animation.AnimationHandler;
import com.mrbysco.armorposer.config.PoserConfig;
import com.mrbysco.armorposer.packets.ArmorStandScreenPayload;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
Expand All @@ -10,6 +12,7 @@
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.entity.decoration.ItemFrame;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
Expand Down Expand Up @@ -51,4 +54,10 @@ public static InteractionResult onPlayerRightClickItem(Player player, Interactio
return InteractionResult.PASS;
}

public static void onFrameUpdate(Entity entity) {
if (!Reference.animationEnabled) return;
if (entity instanceof ItemFrame frame) {
AnimationHandler.onFrameUpdate(frame);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.mrbysco.armorposer.mixin;

import com.mrbysco.armorposer.handlers.EventHandler;
import net.minecraft.world.entity.decoration.BlockAttachedEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Mixin(BlockAttachedEntity.class)
public class BlockAttachedEntityMixin {

@Inject(method = "tick()V", at = @At(
value = "TAIL")
)
public void armorposer$tick(CallbackInfo ci) {
EventHandler.onFrameUpdate(((BlockAttachedEntity) (Object) this));
}
}
Loading
Loading