|
| 1 | +package com.box3lab.block.entity; |
| 2 | + |
| 3 | +import java.util.Locale; |
| 4 | +import java.util.Map; |
| 5 | +import java.util.UUID; |
| 6 | +import java.util.concurrent.ConcurrentHashMap; |
| 7 | + |
| 8 | +import com.box3lab.register.modelbe.PackModelBlockEntityRegistrar; |
| 9 | + |
| 10 | +import net.minecraft.commands.CommandSourceStack; |
| 11 | +import net.minecraft.core.BlockPos; |
| 12 | +import net.minecraft.core.Direction; |
| 13 | +import net.minecraft.network.chat.Component; |
| 14 | +import net.minecraft.server.MinecraftServer; |
| 15 | +import net.minecraft.server.level.ServerLevel; |
| 16 | +import net.minecraft.world.entity.Display; |
| 17 | +import net.minecraft.world.entity.EntityType; |
| 18 | +import net.minecraft.world.item.ItemStack; |
| 19 | +import net.minecraft.world.level.Level; |
| 20 | +import net.minecraft.world.level.block.state.BlockState; |
| 21 | +import net.minecraft.world.level.block.entity.BlockEntity; |
| 22 | +import net.minecraft.world.phys.AABB; |
| 23 | +import net.minecraft.world.phys.Vec3; |
| 24 | + |
| 25 | +public class PackModelBlockEntity extends BlockEntity { |
| 26 | + private static final long RESPAWN_INTERVAL_TICKS = 20L; |
| 27 | + private static final String DISPLAY_TAG_PREFIX = "box3_pack_model:"; |
| 28 | + |
| 29 | + private static final float SCALE_STEP = 0.1F; |
| 30 | + private static final float SCALE_MIN = 0.1F; |
| 31 | + private static final float SCALE_MAX = 4.0F; |
| 32 | + private static final float OFFSET_STEP = 0.05F; |
| 33 | + private static final float ROTATION_STEP = 15.0F; |
| 34 | + private static final Map<UUID, ConfigSnapshot> CONFIG_CLIPBOARD = new ConcurrentHashMap<>(); |
| 35 | + |
| 36 | + private float scale = 1.0F; |
| 37 | + private float offsetX = 0.0F; |
| 38 | + private float offsetY = 0.0F; |
| 39 | + private float offsetZ = 0.0F; |
| 40 | + private float rotationOffset = 0.0F; |
| 41 | + private int modeIndex = 0; |
| 42 | + |
| 43 | + public PackModelBlockEntity(BlockPos pos, BlockState state) { |
| 44 | + super(PackModelBlockEntityRegistrar.typeFor(state.getBlock()), pos, state); |
| 45 | + } |
| 46 | + |
| 47 | + @Override |
| 48 | + public void setRemoved() { |
| 49 | + removeDisplaysAt(this.level, this.getBlockPos()); |
| 50 | + super.setRemoved(); |
| 51 | + } |
| 52 | + |
| 53 | + public static void serverTick(Level level, BlockPos pos, BlockState state, PackModelBlockEntity blockEntity) { |
| 54 | + if (!(level instanceof ServerLevel serverLevel)) { |
| 55 | + return; |
| 56 | + } |
| 57 | + |
| 58 | + String tag = displayTag(pos); |
| 59 | + var displays = findDisplays(serverLevel, pos, tag); |
| 60 | + |
| 61 | + if (displays.isEmpty()) { |
| 62 | + if (serverLevel.getGameTime() % RESPAWN_INTERVAL_TICKS != 0) { |
| 63 | + return; |
| 64 | + } |
| 65 | + spawnDisplay(serverLevel, pos, state, blockEntity, tag); |
| 66 | + return; |
| 67 | + } |
| 68 | + |
| 69 | + for (Display.ItemDisplay display : displays) { |
| 70 | + blockEntity.applyPose(serverLevel, pos, state, display); |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + public void cycleMode(net.minecraft.world.entity.player.Player player) { |
| 75 | + this.modeIndex = (this.modeIndex + 1) % Mode.values().length; |
| 76 | + this.setChanged(); |
| 77 | + player.displayClientMessage(Component.translatable( |
| 78 | + "message.box3.model.config.mode", |
| 79 | + Component.translatable(currentMode().translationKey())), true); |
| 80 | + } |
| 81 | + |
| 82 | + public void adjustCurrentMode(ServerLevel level, BlockPos pos, BlockState state, int direction, |
| 83 | + net.minecraft.world.entity.player.Player player) { |
| 84 | + Mode mode = currentMode(); |
| 85 | + switch (mode) { |
| 86 | + case SCALE -> this.scale = clamp(this.scale + SCALE_STEP * direction, SCALE_MIN, SCALE_MAX); |
| 87 | + case OFFSET_X -> this.offsetX += OFFSET_STEP * direction; |
| 88 | + case OFFSET_Y -> this.offsetY += OFFSET_STEP * direction; |
| 89 | + case OFFSET_Z -> this.offsetZ += OFFSET_STEP * direction; |
| 90 | + case ROTATION -> this.rotationOffset = normalizeDegrees(this.rotationOffset + ROTATION_STEP * direction); |
| 91 | + } |
| 92 | + |
| 93 | + this.setChanged(); |
| 94 | + player.displayClientMessage(statusComponent(), true); |
| 95 | + applyToDisplays(level, pos, state); |
| 96 | + } |
| 97 | + |
| 98 | + public void copyConfig(net.minecraft.world.entity.player.Player player) { |
| 99 | + CONFIG_CLIPBOARD.put(player.getUUID(), |
| 100 | + new ConfigSnapshot(this.scale, this.offsetX, this.offsetY, this.offsetZ, this.rotationOffset)); |
| 101 | + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.success"), true); |
| 102 | + } |
| 103 | + |
| 104 | + public void pasteConfig(ServerLevel level, BlockPos pos, BlockState state, |
| 105 | + net.minecraft.world.entity.player.Player player) { |
| 106 | + ConfigSnapshot snapshot = CONFIG_CLIPBOARD.get(player.getUUID()); |
| 107 | + if (snapshot == null) { |
| 108 | + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.empty"), true); |
| 109 | + return; |
| 110 | + } |
| 111 | + |
| 112 | + this.scale = clamp(snapshot.scale, SCALE_MIN, SCALE_MAX); |
| 113 | + this.offsetX = snapshot.offsetX; |
| 114 | + this.offsetY = snapshot.offsetY; |
| 115 | + this.offsetZ = snapshot.offsetZ; |
| 116 | + this.rotationOffset = normalizeDegrees(snapshot.rotationOffset); |
| 117 | + this.setChanged(); |
| 118 | + |
| 119 | + applyToDisplays(level, pos, state); |
| 120 | + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.pasted"), true); |
| 121 | + player.displayClientMessage(statusComponent(), true); |
| 122 | + } |
| 123 | + |
| 124 | + public static void removeDisplaysAt(Level level, BlockPos pos) { |
| 125 | + if (!(level instanceof ServerLevel serverLevel)) { |
| 126 | + return; |
| 127 | + } |
| 128 | + |
| 129 | + String tag = displayTag(pos); |
| 130 | + for (Display.ItemDisplay display : findDisplays(serverLevel, pos, tag)) { |
| 131 | + display.discard(); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + private static void spawnDisplay(ServerLevel level, BlockPos pos, BlockState state, PackModelBlockEntity be, |
| 136 | + String tag) { |
| 137 | + Display.ItemDisplay display = EntityType.ITEM_DISPLAY.create(level); |
| 138 | + if (display == null) { |
| 139 | + return; |
| 140 | + } |
| 141 | + |
| 142 | + display.setNoGravity(true); |
| 143 | + display.setInvulnerable(true); |
| 144 | + display.getSlot(0).set(new ItemStack(state.getBlock())); |
| 145 | + display.addTag(tag); |
| 146 | + |
| 147 | + be.applyPose(level, pos, state, display); |
| 148 | + level.addFreshEntity(display); |
| 149 | + } |
| 150 | + |
| 151 | + private void applyPose(ServerLevel level, BlockPos pos, BlockState state, Display.ItemDisplay display) { |
| 152 | + double x = pos.getX() + 0.5D; |
| 153 | + double y = pos.getY() + this.offsetY; |
| 154 | + double z = pos.getZ() + 0.5D; |
| 155 | + display.setPos(x, y, z); |
| 156 | + |
| 157 | + float baseYaw = 0.0F; |
| 158 | + if (state.hasProperty(PackModelEntityBlock.HORIZONTAL_FACING)) { |
| 159 | + Direction facing = state.getValue(PackModelEntityBlock.HORIZONTAL_FACING); |
| 160 | + baseYaw = facing.toYRot(); |
| 161 | + } |
| 162 | + display.setYRot(normalizeDegrees(baseYaw + this.rotationOffset)); |
| 163 | + |
| 164 | + applyDisplayTransformation(level, display); |
| 165 | + } |
| 166 | + |
| 167 | + private void applyDisplayTransformation(ServerLevel level, Display.ItemDisplay display) { |
| 168 | + MinecraftServer server = level.getServer(); |
| 169 | + CommandSourceStack source = server.createCommandSourceStack().withSuppressedOutput() |
| 170 | + .withPermission(4); |
| 171 | + |
| 172 | + String cmd = String.format( |
| 173 | + Locale.ROOT, |
| 174 | + "data merge entity %s {item_display:\"fixed\",transformation:{translation:[%sf,%sf,%sf],left_rotation:[0f,0f,0f,1f],scale:[%sf,%sf,%sf],right_rotation:[0f,0f,0f,1f]}}", |
| 175 | + display.getStringUUID(), |
| 176 | + fmt(this.offsetX), fmt(0.0F), fmt(this.offsetZ), |
| 177 | + fmt(this.scale), fmt(this.scale), fmt(this.scale)); |
| 178 | + server.getCommands().performPrefixedCommand(source, cmd); |
| 179 | + } |
| 180 | + |
| 181 | + private static String fmt(float value) { |
| 182 | + return String.format(Locale.ROOT, "%.3f", value); |
| 183 | + } |
| 184 | + |
| 185 | + private static java.util.List<Display.ItemDisplay> findDisplays(ServerLevel level, BlockPos pos, String tag) { |
| 186 | + return level.getEntitiesOfClass( |
| 187 | + Display.ItemDisplay.class, |
| 188 | + new AABB(pos).inflate(0.25D), |
| 189 | + display -> display.getTags().contains(tag)); |
| 190 | + } |
| 191 | + |
| 192 | + private void applyToDisplays(ServerLevel level, BlockPos pos, BlockState state) { |
| 193 | + for (Display.ItemDisplay display : findDisplays(level, pos, displayTag(pos))) { |
| 194 | + applyPose(level, pos, state, display); |
| 195 | + } |
| 196 | + } |
| 197 | + |
| 198 | + private Mode currentMode() { |
| 199 | + return Mode.values()[this.modeIndex]; |
| 200 | + } |
| 201 | + |
| 202 | + private Component statusComponent() { |
| 203 | + return Component.translatable( |
| 204 | + "message.box3.model.config.status", |
| 205 | + Component.translatable(currentMode().translationKey()), |
| 206 | + String.format(Locale.ROOT, "%.2f", this.scale), |
| 207 | + String.format(Locale.ROOT, "%.2f", this.offsetX), |
| 208 | + String.format(Locale.ROOT, "%.2f", this.offsetY), |
| 209 | + String.format(Locale.ROOT, "%.2f", this.offsetZ), |
| 210 | + String.format(Locale.ROOT, "%.1f", this.rotationOffset)); |
| 211 | + } |
| 212 | + |
| 213 | + private static float clamp(float value, float min, float max) { |
| 214 | + return Math.max(min, Math.min(max, value)); |
| 215 | + } |
| 216 | + |
| 217 | + private static float normalizeDegrees(float value) { |
| 218 | + float v = value % 360.0F; |
| 219 | + return v < 0.0F ? v + 360.0F : v; |
| 220 | + } |
| 221 | + |
| 222 | + private static String displayTag(BlockPos pos) { |
| 223 | + return DISPLAY_TAG_PREFIX + pos.asLong(); |
| 224 | + } |
| 225 | + |
| 226 | + private enum Mode { |
| 227 | + SCALE("scale"), |
| 228 | + OFFSET_X("offset_x"), |
| 229 | + OFFSET_Y("offset_y"), |
| 230 | + OFFSET_Z("offset_z"), |
| 231 | + ROTATION("rotation"); |
| 232 | + |
| 233 | + private final String keyPart; |
| 234 | + |
| 235 | + Mode(String keyPart) { |
| 236 | + this.keyPart = keyPart; |
| 237 | + } |
| 238 | + |
| 239 | + public String translationKey() { |
| 240 | + return "message.box3.model.config.mode." + this.keyPart; |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + private record ConfigSnapshot(float scale, float offsetX, float offsetY, float offsetZ, float rotationOffset) { |
| 245 | + } |
| 246 | +} |
0 commit comments