diff --git a/Fabric-1.20.1/gradle.properties b/Fabric-1.20.1/gradle.properties index 4411101..475ba0e 100644 --- a/Fabric-1.20.1/gradle.properties +++ b/Fabric-1.20.1/gradle.properties @@ -12,7 +12,7 @@ loader_version=0.18.4 loom_version=1.15-SNAPSHOT # Mod Properties -mod_version=1.4.1-mc1.20.1 +mod_version=1.4.2-mc1.20.1 maven_group=com.box3lab archives_base_name=box3 diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java b/Fabric-1.20.1/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java new file mode 100644 index 0000000..3f4d71a --- /dev/null +++ b/Fabric-1.20.1/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java @@ -0,0 +1,246 @@ +package com.box3lab.block.entity; + +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import com.box3lab.register.modelbe.PackModelBlockEntityRegistrar; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Display; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +public class PackModelBlockEntity extends BlockEntity { + private static final long RESPAWN_INTERVAL_TICKS = 20L; + private static final String DISPLAY_TAG_PREFIX = "box3_pack_model:"; + + private static final float SCALE_STEP = 0.1F; + private static final float SCALE_MIN = 0.1F; + private static final float SCALE_MAX = 4.0F; + private static final float OFFSET_STEP = 0.05F; + private static final float ROTATION_STEP = 15.0F; + private static final Map CONFIG_CLIPBOARD = new ConcurrentHashMap<>(); + + private float scale = 1.0F; + private float offsetX = 0.0F; + private float offsetY = 0.0F; + private float offsetZ = 0.0F; + private float rotationOffset = 0.0F; + private int modeIndex = 0; + + public PackModelBlockEntity(BlockPos pos, BlockState state) { + super(PackModelBlockEntityRegistrar.typeFor(state.getBlock()), pos, state); + } + + @Override + public void setRemoved() { + removeDisplaysAt(this.level, this.getBlockPos()); + super.setRemoved(); + } + + public static void serverTick(Level level, BlockPos pos, BlockState state, PackModelBlockEntity blockEntity) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + + String tag = displayTag(pos); + var displays = findDisplays(serverLevel, pos, tag); + + if (displays.isEmpty()) { + if (serverLevel.getGameTime() % RESPAWN_INTERVAL_TICKS != 0) { + return; + } + spawnDisplay(serverLevel, pos, state, blockEntity, tag); + return; + } + + for (Display.ItemDisplay display : displays) { + blockEntity.applyPose(serverLevel, pos, state, display); + } + } + + public void cycleMode(net.minecraft.world.entity.player.Player player) { + this.modeIndex = (this.modeIndex + 1) % Mode.values().length; + this.setChanged(); + player.displayClientMessage(Component.translatable( + "message.box3.model.config.mode", + Component.translatable(currentMode().translationKey())), true); + } + + public void adjustCurrentMode(ServerLevel level, BlockPos pos, BlockState state, int direction, + net.minecraft.world.entity.player.Player player) { + Mode mode = currentMode(); + switch (mode) { + case SCALE -> this.scale = clamp(this.scale + SCALE_STEP * direction, SCALE_MIN, SCALE_MAX); + case OFFSET_X -> this.offsetX += OFFSET_STEP * direction; + case OFFSET_Y -> this.offsetY += OFFSET_STEP * direction; + case OFFSET_Z -> this.offsetZ += OFFSET_STEP * direction; + case ROTATION -> this.rotationOffset = normalizeDegrees(this.rotationOffset + ROTATION_STEP * direction); + } + + this.setChanged(); + player.displayClientMessage(statusComponent(), true); + applyToDisplays(level, pos, state); + } + + public void copyConfig(net.minecraft.world.entity.player.Player player) { + CONFIG_CLIPBOARD.put(player.getUUID(), + new ConfigSnapshot(this.scale, this.offsetX, this.offsetY, this.offsetZ, this.rotationOffset)); + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.success"), true); + } + + public void pasteConfig(ServerLevel level, BlockPos pos, BlockState state, + net.minecraft.world.entity.player.Player player) { + ConfigSnapshot snapshot = CONFIG_CLIPBOARD.get(player.getUUID()); + if (snapshot == null) { + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.empty"), true); + return; + } + + this.scale = clamp(snapshot.scale, SCALE_MIN, SCALE_MAX); + this.offsetX = snapshot.offsetX; + this.offsetY = snapshot.offsetY; + this.offsetZ = snapshot.offsetZ; + this.rotationOffset = normalizeDegrees(snapshot.rotationOffset); + this.setChanged(); + + applyToDisplays(level, pos, state); + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.pasted"), true); + player.displayClientMessage(statusComponent(), true); + } + + public static void removeDisplaysAt(Level level, BlockPos pos) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + + String tag = displayTag(pos); + for (Display.ItemDisplay display : findDisplays(serverLevel, pos, tag)) { + display.discard(); + } + } + + private static void spawnDisplay(ServerLevel level, BlockPos pos, BlockState state, PackModelBlockEntity be, + String tag) { + Display.ItemDisplay display = EntityType.ITEM_DISPLAY.create(level); + if (display == null) { + return; + } + + display.setNoGravity(true); + display.setInvulnerable(true); + display.getSlot(0).set(new ItemStack(state.getBlock())); + display.addTag(tag); + + be.applyPose(level, pos, state, display); + level.addFreshEntity(display); + } + + private void applyPose(ServerLevel level, BlockPos pos, BlockState state, Display.ItemDisplay display) { + double x = pos.getX() + 0.5D; + double y = pos.getY() + this.offsetY; + double z = pos.getZ() + 0.5D; + display.setPos(x, y, z); + + float baseYaw = 0.0F; + if (state.hasProperty(PackModelEntityBlock.HORIZONTAL_FACING)) { + Direction facing = state.getValue(PackModelEntityBlock.HORIZONTAL_FACING); + baseYaw = facing.toYRot(); + } + display.setYRot(normalizeDegrees(baseYaw + this.rotationOffset)); + + applyDisplayTransformation(level, display); + } + + private void applyDisplayTransformation(ServerLevel level, Display.ItemDisplay display) { + MinecraftServer server = level.getServer(); + CommandSourceStack source = server.createCommandSourceStack().withSuppressedOutput() + .withPermission(4); + + String cmd = String.format( + Locale.ROOT, + "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]}}", + display.getStringUUID(), + fmt(this.offsetX), fmt(0.0F), fmt(this.offsetZ), + fmt(this.scale), fmt(this.scale), fmt(this.scale)); + server.getCommands().performPrefixedCommand(source, cmd); + } + + private static String fmt(float value) { + return String.format(Locale.ROOT, "%.3f", value); + } + + private static java.util.List findDisplays(ServerLevel level, BlockPos pos, String tag) { + return level.getEntitiesOfClass( + Display.ItemDisplay.class, + new AABB(pos).inflate(0.25D), + display -> display.getTags().contains(tag)); + } + + private void applyToDisplays(ServerLevel level, BlockPos pos, BlockState state) { + for (Display.ItemDisplay display : findDisplays(level, pos, displayTag(pos))) { + applyPose(level, pos, state, display); + } + } + + private Mode currentMode() { + return Mode.values()[this.modeIndex]; + } + + private Component statusComponent() { + return Component.translatable( + "message.box3.model.config.status", + Component.translatable(currentMode().translationKey()), + String.format(Locale.ROOT, "%.2f", this.scale), + String.format(Locale.ROOT, "%.2f", this.offsetX), + String.format(Locale.ROOT, "%.2f", this.offsetY), + String.format(Locale.ROOT, "%.2f", this.offsetZ), + String.format(Locale.ROOT, "%.1f", this.rotationOffset)); + } + + private static float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } + + private static float normalizeDegrees(float value) { + float v = value % 360.0F; + return v < 0.0F ? v + 360.0F : v; + } + + private static String displayTag(BlockPos pos) { + return DISPLAY_TAG_PREFIX + pos.asLong(); + } + + private enum Mode { + SCALE("scale"), + OFFSET_X("offset_x"), + OFFSET_Y("offset_y"), + OFFSET_Z("offset_z"), + ROTATION("rotation"); + + private final String keyPart; + + Mode(String keyPart) { + this.keyPart = keyPart; + } + + public String translationKey() { + return "message.box3.model.config.mode." + this.keyPart; + } + } + + private record ConfigSnapshot(float scale, float offsetX, float offsetY, float offsetZ, float rotationOffset) { + } +} diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java b/Fabric-1.20.1/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java new file mode 100644 index 0000000..22a8a60 --- /dev/null +++ b/Fabric-1.20.1/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java @@ -0,0 +1,157 @@ +package com.box3lab.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; + +public class PackModelEntityBlock extends Block implements EntityBlock { + public static final EnumProperty HORIZONTAL_FACING = BlockStateProperties.HORIZONTAL_FACING; + + public PackModelEntityBlock(BlockBehaviour.Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(HORIZONTAL_FACING, Direction.NORTH)); + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new PackModelBlockEntity(pos, state); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + protected void spawnDestroyParticles(Level level, Player player, BlockPos pos, BlockState state) { + // This block is rendered by ItemDisplay instead of block model, + // so vanilla block-break particles would resolve to missing textures. + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(HORIZONTAL_FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(HORIZONTAL_FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(HORIZONTAL_FACING, rotation.rotate(state.getValue(HORIZONTAL_FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(HORIZONTAL_FACING))); + } + + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, + InteractionHand hand, BlockHitResult hitResult) { + if (hand != InteractionHand.MAIN_HAND) { + return InteractionResult.PASS; + } + + ItemStack stack = player.getItemInHand(hand); + BlockEntity be = level.getBlockEntity(pos); + if (!(be instanceof PackModelBlockEntity modelBe)) { + return InteractionResult.PASS; + } + + if (stack.is(Items.PAPER)) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + modelBe.copyConfig(player); + return InteractionResult.SUCCESS; + } + + if (stack.is(Items.BOOK)) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (level instanceof net.minecraft.server.level.ServerLevel serverLevel) { + modelBe.pasteConfig(serverLevel, pos, state, player); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + if (stack.is(Items.STICK)) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (level instanceof net.minecraft.server.level.ServerLevel serverLevel) { + modelBe.adjustCurrentMode(serverLevel, pos, state, 1, player); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + if (stack.is(Items.BLAZE_ROD)) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (level instanceof net.minecraft.server.level.ServerLevel serverLevel) { + modelBe.adjustCurrentMode(serverLevel, pos, state, -1, player); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + if (!stack.isEmpty()) { + return InteractionResult.PASS; + } + + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + + modelBe.cycleMode(player); + return InteractionResult.SUCCESS; + } + + @Override + @SuppressWarnings("unchecked") + public BlockEntityTicker getTicker(Level level, BlockState state, + BlockEntityType blockEntityType) { + if (level.isClientSide()) { + return null; + } + + BlockEntityType expectedType = com.box3lab.register.modelbe.PackModelBlockEntityRegistrar + .typeFor(state.getBlock()); + if (blockEntityType != expectedType) { + return null; + } + + return (lvl, pos, blockState, be) -> PackModelBlockEntity.serverTick( + lvl, + pos, + blockState, + (PackModelBlockEntity) be); + } +} diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/item/ModelDestroyerItem.java b/Fabric-1.20.1/src/main/java/com/box3lab/item/ModelDestroyerItem.java deleted file mode 100644 index b70e5de..0000000 --- a/Fabric-1.20.1/src/main/java/com/box3lab/item/ModelDestroyerItem.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.box3lab.item; - -import java.util.List; - -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.Display; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; - -public class ModelDestroyerItem extends Item { - private static final double RANGE = 10.0; - - public ModelDestroyerItem(Properties properties) { - super(properties); - } - - @Override - public InteractionResult useOn(UseOnContext context) { - Level level = context.getLevel(); - if (!(level instanceof ServerLevel serverLevel)) { - return InteractionResult.SUCCESS; - } - - Player player = context.getPlayer(); - if (player == null) { - return InteractionResult.PASS; - } - - boolean deleted = deleteTarget(serverLevel, player); - return deleted ? InteractionResult.SUCCESS : InteractionResult.PASS; - } - - private static boolean deleteTarget(ServerLevel level, Player player) { - Display.ItemDisplay target = findDisplay(level, player, RANGE); - if (target == null) { - return false; - } - target.discard(); - return true; - } - - private static Display.ItemDisplay findDisplay(ServerLevel level, Player player, double range) { - Vec3 start = player.getEyePosition(1.0f); - Vec3 look = player.getViewVector(1.0f); - Vec3 end = start.add(look.scale(range)); - AABB searchBox = player.getBoundingBox().expandTowards(look.scale(range)).inflate(1.5); - - List displays = level.getEntitiesOfClass(Display.ItemDisplay.class, searchBox); - Display.ItemDisplay closest = null; - double bestDist = range * range; - - for (Display.ItemDisplay display : displays) { - AABB box = display.getBoundingBox().inflate(0.8); - var hit = box.clip(start, end); - if (hit.isEmpty()) { - continue; - } - double dist = start.distanceToSqr(hit.get()); - if (dist < bestDist) { - bestDist = dist; - closest = display; - } - } - - return closest; - } -} diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/item/ModelDisplayItem.java b/Fabric-1.20.1/src/main/java/com/box3lab/item/ModelDisplayItem.java deleted file mode 100644 index 7e2d5ca..0000000 --- a/Fabric-1.20.1/src/main/java/com/box3lab/item/ModelDisplayItem.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.box3lab.item; - -import net.minecraft.core.BlockPos; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.Display; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.Vec3; - -public class ModelDisplayItem extends Item { - public ModelDisplayItem(Properties properties) { - super(properties); - } - - @Override - public InteractionResult useOn(UseOnContext context) { - Level level = context.getLevel(); - if (!(level instanceof ServerLevel serverLevel)) { - return InteractionResult.SUCCESS; - } - - Display.ItemDisplay display = (Display.ItemDisplay) EntityType.ITEM_DISPLAY.create(serverLevel); - if (display == null) { - return InteractionResult.FAIL; - } - - BlockPos placePos = context.getClickedPos().relative(context.getClickedFace()); - Vec3 pos = Vec3.atCenterOf(placePos); - display.setPos(pos.x, pos.y, pos.z); - - Player player = context.getPlayer(); - if (player != null) { - display.setYRot(player.getYRot()); - } - - ItemStack displayStack = context.getItemInHand().copyWithCount(1); - display.getSlot(0).set(displayStack); - - display.setNoGravity(true); - serverLevel.addFreshEntity(display); - - if (player == null || !player.getAbilities().instabuild) { - context.getItemInHand().shrink(1); - } - - return InteractionResult.SUCCESS; - } -} diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/register/ModBlocks.java b/Fabric-1.20.1/src/main/java/com/box3lab/register/ModBlocks.java index f815bbb..2853bc1 100644 --- a/Fabric-1.20.1/src/main/java/com/box3lab/register/ModBlocks.java +++ b/Fabric-1.20.1/src/main/java/com/box3lab/register/ModBlocks.java @@ -6,6 +6,7 @@ import com.box3lab.Box3; import com.box3lab.register.core.BlockRegistrar; import com.box3lab.register.creative.CreativeTabRegistrar; +import com.box3lab.register.modelbe.PackModelBlockEntityRegistrar; import com.box3lab.register.sound.CategorySoundTypes; import com.box3lab.register.voxel.VoxelBlockFactories; import com.box3lab.register.voxel.VoxelBlockPropertiesFactory; @@ -62,6 +63,7 @@ public static void initialize() { } CreativeTabRegistrar.registerCreativeTabs(Box3.MOD_ID, BLOCKS, data); + PackModelBlockEntityRegistrar.registerAll(); } -} \ No newline at end of file +} diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/register/ModItems.java b/Fabric-1.20.1/src/main/java/com/box3lab/register/ModItems.java index 798e513..f8f2634 100644 --- a/Fabric-1.20.1/src/main/java/com/box3lab/register/ModItems.java +++ b/Fabric-1.20.1/src/main/java/com/box3lab/register/ModItems.java @@ -6,9 +6,6 @@ private ModItems() { } public static void initialize() { - // Register all model-based display items discovered from resource packs - ModelItemRegistrar.registerAll(); - - ModelToolRegistrar.registerAll(); + // Reserved for future item registrations. } } diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/register/ModelItemRegistrar.java b/Fabric-1.20.1/src/main/java/com/box3lab/register/ModelItemRegistrar.java deleted file mode 100644 index 895e5ac..0000000 --- a/Fabric-1.20.1/src/main/java/com/box3lab/register/ModelItemRegistrar.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.box3lab.register; - -import java.io.File; -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Enumeration; -import java.util.LinkedHashSet; -import java.util.Locale; -import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import com.box3lab.Box3; -import com.box3lab.item.ModelDisplayItem; -import com.box3lab.register.creative.CreativeTabExtras; -import com.box3lab.register.creative.CreativeTabRegistrar; - -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; - -public final class ModelItemRegistrar { - private static final String ITEMS_DIR_PREFIX = "assets/" + Box3.MOD_ID + "/items/"; - public static final String DEFAULT_TAB = "models"; - - private ModelItemRegistrar() { - } - - public static void registerAll() { - Set itemPaths = discoverModelItemPaths(); - if (itemPaths.isEmpty()) { - return; - } - - for (String path : itemPaths) { - ResourceLocation id; - try { - id = new ResourceLocation(Box3.MOD_ID, path); - } catch (IllegalArgumentException e) { - continue; - } - - if (BuiltInRegistries.ITEM.containsKey(id)) { - continue; - } - - Item item = new ModelDisplayItem(new Item.Properties()); - Registry.register(BuiltInRegistries.ITEM, id, item); - CreativeTabExtras.add(DEFAULT_TAB, item); - } - - CreativeTabRegistrar.registerModelTab(Box3.MOD_ID); - } - - private static Set discoverModelItemPaths() { - Set results = new LinkedHashSet<>(); - Path resourcepacksDir = FabricLoader.getInstance().getGameDir().resolve("resourcepacks"); - if (!Files.isDirectory(resourcepacksDir)) { - return results; - } - - try (DirectoryStream stream = Files.newDirectoryStream(resourcepacksDir)) { - for (Path entry : stream) { - if (Files.isDirectory(entry)) { - scanDirectoryPack(entry, results); - } else if (isArchive(entry)) { - scanZipPack(entry, results); - } - } - } catch (IOException ignored) { - } - - return results; - } - - private static boolean isArchive(Path path) { - String name = path.getFileName().toString().toLowerCase(Locale.ROOT); - return name.endsWith(".zip") || name.endsWith(".jar"); - } - - private static void scanDirectoryPack(Path packDir, Set out) { - Path itemsDir = packDir.resolve("assets").resolve(Box3.MOD_ID).resolve("items"); - if (!Files.isDirectory(itemsDir)) { - return; - } - - try (var paths = Files.walk(itemsDir)) { - paths.filter(Files::isRegularFile) - .forEach(file -> { - String name = file.getFileName().toString(); - if (!name.endsWith(".json")) { - return; - } - - String rel = itemsDir.relativize(file).toString().replace(File.separatorChar, '/'); - if (rel.endsWith(".json")) { - rel = rel.substring(0, rel.length() - 5); - } - if (!rel.isBlank()) { - out.add(rel); - } - }); - } catch (IOException ignored) { - } - } - - private static void scanZipPack(Path zipPath, Set out) { - try (ZipFile zip = new ZipFile(zipPath.toFile())) { - Enumeration entries = zip.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (entry.isDirectory()) { - continue; - } - String name = entry.getName(); - if (!name.startsWith(ITEMS_DIR_PREFIX) || !name.endsWith(".json")) { - continue; - } - String rel = name.substring(ITEMS_DIR_PREFIX.length(), name.length() - 5); - if (!rel.isBlank()) { - out.add(rel); - } - } - } catch (IOException ignored) { - } - } -} diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/register/ModelToolRegistrar.java b/Fabric-1.20.1/src/main/java/com/box3lab/register/ModelToolRegistrar.java deleted file mode 100644 index c8ed800..0000000 --- a/Fabric-1.20.1/src/main/java/com/box3lab/register/ModelToolRegistrar.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.box3lab.register; - -import com.box3lab.Box3; -import com.box3lab.item.ModelDestroyerItem; -import com.box3lab.register.creative.CreativeTabExtras; - -import net.fabricmc.fabric.api.event.player.UseEntityCallback; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.Display; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; - -public final class ModelToolRegistrar { - private ModelToolRegistrar() { - } - - public static void registerAll() { - registerDestroyer(); - registerDestroyerHandler(); - } - - private static void registerDestroyer() { - ResourceLocation id = new ResourceLocation(Box3.MOD_ID, "model_destroyer"); - if (BuiltInRegistries.ITEM.containsKey(id)) { - return; - } - - Item item = new ModelDestroyerItem(new Item.Properties().stacksTo(1)); - Registry.register(BuiltInRegistries.ITEM, id, item); - CreativeTabExtras.add(ModelItemRegistrar.DEFAULT_TAB, item); - } - - private static void registerDestroyerHandler() { - UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { - ItemStack stack = player.getItemInHand(hand); - if (!(stack.getItem() instanceof ModelDestroyerItem)) { - return InteractionResult.PASS; - } - - if (!(entity instanceof Display.ItemDisplay display)) { - return InteractionResult.PASS; - } - - if (!(world instanceof ServerLevel)) { - return InteractionResult.SUCCESS; - } - - display.discard(); - return InteractionResult.SUCCESS; - }); - } -} diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java b/Fabric-1.20.1/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java index 3560e16..2560bf6 100644 --- a/Fabric-1.20.1/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java +++ b/Fabric-1.20.1/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java @@ -7,7 +7,6 @@ import java.util.Locale; import java.util.Map; -import static com.box3lab.register.ModelItemRegistrar.DEFAULT_TAB; import com.box3lab.util.BlockIndexData; import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; @@ -23,6 +22,8 @@ import net.minecraft.world.level.block.Block; public final class CreativeTabRegistrar { + public static final String DEFAULT_MODEL_TAB = "models"; + private CreativeTabRegistrar() { } @@ -95,7 +96,7 @@ public static void registerCreativeTabs(String modId, Map blocks, } public static void registerModelTab(String modId) { - String categoryPath = sanitizeCategoryPath(DEFAULT_TAB); + String categoryPath = sanitizeCategoryPath(DEFAULT_MODEL_TAB); if (categoryPath.isBlank()) { return; } diff --git a/Fabric-1.20.1/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java b/Fabric-1.20.1/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java new file mode 100644 index 0000000..e897554 --- /dev/null +++ b/Fabric-1.20.1/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java @@ -0,0 +1,223 @@ +package com.box3lab.register.modelbe; + +import static com.box3lab.Box3.MOD_ID; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import com.box3lab.block.entity.PackModelEntityBlock; +import com.box3lab.register.creative.CreativeTabExtras; +import com.box3lab.register.creative.CreativeTabRegistrar; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.MapColor; + +public final class PackModelBlockEntityRegistrar { + private static final String ASSET_PREFIX = "assets/" + MOD_ID + "/"; + + private static final Map> TYPES_BY_BLOCK = new LinkedHashMap<>(); + + private PackModelBlockEntityRegistrar() { + } + + public static void registerAll() { + Set modelNames = discoverPairedModelNames(); + if (modelNames.isEmpty()) { + return; + } + + for (String name : modelNames) { + ResourceLocation id = ResourceLocation.tryBuild(MOD_ID, name); + if (id == null) { + continue; + } + + ResourceKey blockKey = ResourceKey.create(Registries.BLOCK, id); + if (BuiltInRegistries.BLOCK.containsKey(blockKey)) { + continue; + } + + Block block = new PackModelEntityBlock( + BlockBehaviour.Properties.of() + .mapColor(MapColor.STONE) + .strength(1.5F, 6.0F) + .noOcclusion() + .isViewBlocking((state, level, pos) -> false) + .isSuffocating((state, level, pos) -> false) + .isRedstoneConductor((state, level, pos) -> false)); + Registry.register(BuiltInRegistries.BLOCK, blockKey, block); + + ResourceKey itemKey = ResourceKey.create( + Registries.ITEM, + new ResourceLocation(MOD_ID, name)); + if (!BuiltInRegistries.ITEM.containsKey(itemKey)) { + final String itemDescriptionId = "item." + MOD_ID + "." + name; + Item item = new BlockItem(block, new Item.Properties()) { + @Override + public String getDescriptionId() { + return itemDescriptionId; + } + + @Override + public String getDescriptionId(ItemStack stack) { + return itemDescriptionId; + } + }; + Registry.register(BuiltInRegistries.ITEM, itemKey, item); + CreativeTabExtras.add(CreativeTabRegistrar.DEFAULT_MODEL_TAB, item); + } + + ResourceKey> blockEntityKey = ResourceKey.create(Registries.BLOCK_ENTITY_TYPE, id); + if (BuiltInRegistries.BLOCK_ENTITY_TYPE.containsKey(blockEntityKey)) { + continue; + } + + BlockEntityType type = FabricBlockEntityTypeBuilder + .create(com.box3lab.block.entity.PackModelBlockEntity::new, block) + .build(); + + Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE, blockEntityKey, type); + TYPES_BY_BLOCK.put(block, type); + } + + CreativeTabRegistrar.registerModelTab(MOD_ID); + } + + public static BlockEntityType typeFor(Block block) { + BlockEntityType type = TYPES_BY_BLOCK.get(block); + if (type == null) { + throw new IllegalStateException("No block entity type bound for block: " + block); + } + return type; + } + + private static Set discoverPairedModelNames() { + Set result = new LinkedHashSet<>(); + Path packsRoot = FabricLoader.getInstance().getGameDir().resolve("resourcepacks"); + if (!Files.isDirectory(packsRoot)) { + return result; + } + + try (var entries = Files.list(packsRoot)) { + entries.forEach(entry -> { + if (Files.isDirectory(entry)) { + collectFromDirectory(entry, result); + } else if (isArchive(entry)) { + collectFromArchive(entry, result); + } + }); + } catch (IOException ignored) { + } + + return result; + } + + private static void collectFromDirectory(Path packDir, Set out) { + Path assetsRoot = packDir.resolve("assets").resolve(MOD_ID); + if (!Files.isDirectory(assetsRoot)) { + return; + } + + Set models = collectBaseNamesFromDirectory(assetsRoot, ".json"); + if (models.isEmpty()) { + return; + } + + Set textures = collectBaseNamesFromDirectory(assetsRoot, ".png"); + if (textures.isEmpty()) { + return; + } + + for (String model : models) { + if (textures.contains(model)) { + out.add(model); + } + } + } + + private static Set collectBaseNamesFromDirectory(Path root, String suffix) { + Set names = new LinkedHashSet<>(); + try (var files = Files.walk(root)) { + files.filter(Files::isRegularFile).forEach(path -> { + String fileName = path.getFileName().toString(); + if (!fileName.toLowerCase(Locale.ROOT).endsWith(suffix)) { + return; + } + + String base = fileName.substring(0, fileName.length() - suffix.length()).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + names.add(base); + } + }); + } catch (IOException ignored) { + } + return names; + } + + private static void collectFromArchive(Path archive, Set out) { + try (ZipFile zip = new ZipFile(archive.toFile())) { + Set models = new LinkedHashSet<>(); + Set textures = new LinkedHashSet<>(); + + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + continue; + } + + String name = entry.getName(); + if (!name.startsWith(ASSET_PREFIX)) { + continue; + } + + String fileName = name.substring(name.lastIndexOf('/') + 1); + String lower = fileName.toLowerCase(Locale.ROOT); + if (lower.endsWith(".json")) { + String base = fileName.substring(0, fileName.length() - 5).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + models.add(base); + } + } else if (lower.endsWith(".png")) { + String base = fileName.substring(0, fileName.length() - 4).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + textures.add(base); + } + } + } + + for (String model : models) { + if (textures.contains(model)) { + out.add(model); + } + } + } catch (IOException ignored) { + } + } + + private static boolean isArchive(Path path) { + String name = path.getFileName().toString().toLowerCase(Locale.ROOT); + return name.endsWith(".zip") || name.endsWith(".jar"); + } +} diff --git a/Fabric-1.20.1/src/main/resources/assets/box3/lang/en_us.json b/Fabric-1.20.1/src/main/resources/assets/box3/lang/en_us.json index 9b34656..00c4e98 100644 --- a/Fabric-1.20.1/src/main/resources/assets/box3/lang/en_us.json +++ b/Fabric-1.20.1/src/main/resources/assets/box3/lang/en_us.json @@ -411,6 +411,15 @@ "command.box3.box3barrier.status": "Barrier visible: %s", "command.box3.box3barrier.set": "Barrier visibility set to: %s", "command.box3.box3barrier.toggled": "Barrier visibility toggled to: %s (re-enter the world to fully apply)", - "item.box3.model_destroyer": "Model destruction bucket", + "message.box3.model.config.mode": "Model config mode: %s (Stick: +, Blaze Rod: -, Paper: copy, Book: paste)", + "message.box3.model.config.status": "mode=%s scale=%s offset=(%s, %s, %s) rot=%s", + "message.box3.model.config.mode.scale": "Scale", + "message.box3.model.config.mode.offset_x": "Offset X", + "message.box3.model.config.mode.offset_y": "Offset Y", + "message.box3.model.config.mode.offset_z": "Offset Z", + "message.box3.model.config.mode.rotation": "Rotation", + "message.box3.model.config.copy.success": "Copied current model config.", + "message.box3.model.config.copy.empty": "No copied config found.", + "message.box3.model.config.copy.pasted": "Pasted copied model config.", "flat_world_preset.box3.box3_plains_world": "Box3 Plains" } diff --git a/Fabric-1.20.1/src/main/resources/assets/box3/lang/zh_cn.json b/Fabric-1.20.1/src/main/resources/assets/box3/lang/zh_cn.json index 29bebcc..82b8d1e 100644 --- a/Fabric-1.20.1/src/main/resources/assets/box3/lang/zh_cn.json +++ b/Fabric-1.20.1/src/main/resources/assets/box3/lang/zh_cn.json @@ -399,6 +399,15 @@ "command.box3.box3barrier.status": "屏障可见状态:%s", "command.box3.box3barrier.set": "屏障可见状态已设置为:%s", "command.box3.box3barrier.toggled": "屏障可见状态已切换为:%s(重新进入世界以完全生效)", - "item.box3.model_destroyer": "模型销毁桶", + "message.box3.model.config.mode": "模型配置模式:%s(木棍:增加,烈焰棒:减少,纸:复制,书:粘贴)", + "message.box3.model.config.status": "模式=%s 缩放=%s 偏移=(%s, %s, %s) 旋转=%s", + "message.box3.model.config.mode.scale": "缩放", + "message.box3.model.config.mode.offset_x": "X偏移", + "message.box3.model.config.mode.offset_y": "Y偏移", + "message.box3.model.config.mode.offset_z": "Z偏移", + "message.box3.model.config.mode.rotation": "旋转", + "message.box3.model.config.copy.success": "已复制当前模型参数。", + "message.box3.model.config.copy.empty": "没有可粘贴的已复制参数。", + "message.box3.model.config.copy.pasted": "已粘贴模型参数。", "flat_world_preset.box3.box3_plains_world": "神岛平原" } diff --git a/Fabric-1.20.1/src/main/resources/data/minecraft/tags/worldgen/flat_level_generator_preset/visible.json b/Fabric-1.20.1/src/main/resources/data/minecraft/tags/worldgen/flat_level_generator_preset/visible.json index 7809017..3fa8ad8 100644 --- a/Fabric-1.20.1/src/main/resources/data/minecraft/tags/worldgen/flat_level_generator_preset/visible.json +++ b/Fabric-1.20.1/src/main/resources/data/minecraft/tags/worldgen/flat_level_generator_preset/visible.json @@ -1,4 +1,4 @@ { "replace": false, - "values": ["box3:box3_plains_world", "box3:box3_custom_noise"] + "values": ["box3:box3_plains_world"] } diff --git a/Fabric-1.21.1/gradle.properties b/Fabric-1.21.1/gradle.properties index 4d1d24b..b5cd0af 100644 --- a/Fabric-1.21.1/gradle.properties +++ b/Fabric-1.21.1/gradle.properties @@ -12,7 +12,7 @@ loader_version=0.18.4 loom_version=1.15-SNAPSHOT # Mod Properties -mod_version=1.4.1-mc1.21.1 +mod_version=1.4.2-mc1.21.1 maven_group=com.box3lab archives_base_name=box3 diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java b/Fabric-1.21.1/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java new file mode 100644 index 0000000..3f4d71a --- /dev/null +++ b/Fabric-1.21.1/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java @@ -0,0 +1,246 @@ +package com.box3lab.block.entity; + +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import com.box3lab.register.modelbe.PackModelBlockEntityRegistrar; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Display; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +public class PackModelBlockEntity extends BlockEntity { + private static final long RESPAWN_INTERVAL_TICKS = 20L; + private static final String DISPLAY_TAG_PREFIX = "box3_pack_model:"; + + private static final float SCALE_STEP = 0.1F; + private static final float SCALE_MIN = 0.1F; + private static final float SCALE_MAX = 4.0F; + private static final float OFFSET_STEP = 0.05F; + private static final float ROTATION_STEP = 15.0F; + private static final Map CONFIG_CLIPBOARD = new ConcurrentHashMap<>(); + + private float scale = 1.0F; + private float offsetX = 0.0F; + private float offsetY = 0.0F; + private float offsetZ = 0.0F; + private float rotationOffset = 0.0F; + private int modeIndex = 0; + + public PackModelBlockEntity(BlockPos pos, BlockState state) { + super(PackModelBlockEntityRegistrar.typeFor(state.getBlock()), pos, state); + } + + @Override + public void setRemoved() { + removeDisplaysAt(this.level, this.getBlockPos()); + super.setRemoved(); + } + + public static void serverTick(Level level, BlockPos pos, BlockState state, PackModelBlockEntity blockEntity) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + + String tag = displayTag(pos); + var displays = findDisplays(serverLevel, pos, tag); + + if (displays.isEmpty()) { + if (serverLevel.getGameTime() % RESPAWN_INTERVAL_TICKS != 0) { + return; + } + spawnDisplay(serverLevel, pos, state, blockEntity, tag); + return; + } + + for (Display.ItemDisplay display : displays) { + blockEntity.applyPose(serverLevel, pos, state, display); + } + } + + public void cycleMode(net.minecraft.world.entity.player.Player player) { + this.modeIndex = (this.modeIndex + 1) % Mode.values().length; + this.setChanged(); + player.displayClientMessage(Component.translatable( + "message.box3.model.config.mode", + Component.translatable(currentMode().translationKey())), true); + } + + public void adjustCurrentMode(ServerLevel level, BlockPos pos, BlockState state, int direction, + net.minecraft.world.entity.player.Player player) { + Mode mode = currentMode(); + switch (mode) { + case SCALE -> this.scale = clamp(this.scale + SCALE_STEP * direction, SCALE_MIN, SCALE_MAX); + case OFFSET_X -> this.offsetX += OFFSET_STEP * direction; + case OFFSET_Y -> this.offsetY += OFFSET_STEP * direction; + case OFFSET_Z -> this.offsetZ += OFFSET_STEP * direction; + case ROTATION -> this.rotationOffset = normalizeDegrees(this.rotationOffset + ROTATION_STEP * direction); + } + + this.setChanged(); + player.displayClientMessage(statusComponent(), true); + applyToDisplays(level, pos, state); + } + + public void copyConfig(net.minecraft.world.entity.player.Player player) { + CONFIG_CLIPBOARD.put(player.getUUID(), + new ConfigSnapshot(this.scale, this.offsetX, this.offsetY, this.offsetZ, this.rotationOffset)); + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.success"), true); + } + + public void pasteConfig(ServerLevel level, BlockPos pos, BlockState state, + net.minecraft.world.entity.player.Player player) { + ConfigSnapshot snapshot = CONFIG_CLIPBOARD.get(player.getUUID()); + if (snapshot == null) { + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.empty"), true); + return; + } + + this.scale = clamp(snapshot.scale, SCALE_MIN, SCALE_MAX); + this.offsetX = snapshot.offsetX; + this.offsetY = snapshot.offsetY; + this.offsetZ = snapshot.offsetZ; + this.rotationOffset = normalizeDegrees(snapshot.rotationOffset); + this.setChanged(); + + applyToDisplays(level, pos, state); + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.pasted"), true); + player.displayClientMessage(statusComponent(), true); + } + + public static void removeDisplaysAt(Level level, BlockPos pos) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + + String tag = displayTag(pos); + for (Display.ItemDisplay display : findDisplays(serverLevel, pos, tag)) { + display.discard(); + } + } + + private static void spawnDisplay(ServerLevel level, BlockPos pos, BlockState state, PackModelBlockEntity be, + String tag) { + Display.ItemDisplay display = EntityType.ITEM_DISPLAY.create(level); + if (display == null) { + return; + } + + display.setNoGravity(true); + display.setInvulnerable(true); + display.getSlot(0).set(new ItemStack(state.getBlock())); + display.addTag(tag); + + be.applyPose(level, pos, state, display); + level.addFreshEntity(display); + } + + private void applyPose(ServerLevel level, BlockPos pos, BlockState state, Display.ItemDisplay display) { + double x = pos.getX() + 0.5D; + double y = pos.getY() + this.offsetY; + double z = pos.getZ() + 0.5D; + display.setPos(x, y, z); + + float baseYaw = 0.0F; + if (state.hasProperty(PackModelEntityBlock.HORIZONTAL_FACING)) { + Direction facing = state.getValue(PackModelEntityBlock.HORIZONTAL_FACING); + baseYaw = facing.toYRot(); + } + display.setYRot(normalizeDegrees(baseYaw + this.rotationOffset)); + + applyDisplayTransformation(level, display); + } + + private void applyDisplayTransformation(ServerLevel level, Display.ItemDisplay display) { + MinecraftServer server = level.getServer(); + CommandSourceStack source = server.createCommandSourceStack().withSuppressedOutput() + .withPermission(4); + + String cmd = String.format( + Locale.ROOT, + "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]}}", + display.getStringUUID(), + fmt(this.offsetX), fmt(0.0F), fmt(this.offsetZ), + fmt(this.scale), fmt(this.scale), fmt(this.scale)); + server.getCommands().performPrefixedCommand(source, cmd); + } + + private static String fmt(float value) { + return String.format(Locale.ROOT, "%.3f", value); + } + + private static java.util.List findDisplays(ServerLevel level, BlockPos pos, String tag) { + return level.getEntitiesOfClass( + Display.ItemDisplay.class, + new AABB(pos).inflate(0.25D), + display -> display.getTags().contains(tag)); + } + + private void applyToDisplays(ServerLevel level, BlockPos pos, BlockState state) { + for (Display.ItemDisplay display : findDisplays(level, pos, displayTag(pos))) { + applyPose(level, pos, state, display); + } + } + + private Mode currentMode() { + return Mode.values()[this.modeIndex]; + } + + private Component statusComponent() { + return Component.translatable( + "message.box3.model.config.status", + Component.translatable(currentMode().translationKey()), + String.format(Locale.ROOT, "%.2f", this.scale), + String.format(Locale.ROOT, "%.2f", this.offsetX), + String.format(Locale.ROOT, "%.2f", this.offsetY), + String.format(Locale.ROOT, "%.2f", this.offsetZ), + String.format(Locale.ROOT, "%.1f", this.rotationOffset)); + } + + private static float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } + + private static float normalizeDegrees(float value) { + float v = value % 360.0F; + return v < 0.0F ? v + 360.0F : v; + } + + private static String displayTag(BlockPos pos) { + return DISPLAY_TAG_PREFIX + pos.asLong(); + } + + private enum Mode { + SCALE("scale"), + OFFSET_X("offset_x"), + OFFSET_Y("offset_y"), + OFFSET_Z("offset_z"), + ROTATION("rotation"); + + private final String keyPart; + + Mode(String keyPart) { + this.keyPart = keyPart; + } + + public String translationKey() { + return "message.box3.model.config.mode." + this.keyPart; + } + } + + private record ConfigSnapshot(float scale, float offsetX, float offsetY, float offsetZ, float rotationOffset) { + } +} diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java b/Fabric-1.21.1/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java new file mode 100644 index 0000000..6a9f2ea --- /dev/null +++ b/Fabric-1.21.1/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java @@ -0,0 +1,160 @@ +package com.box3lab.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; + +public class PackModelEntityBlock extends Block implements EntityBlock { + public static final EnumProperty HORIZONTAL_FACING = BlockStateProperties.HORIZONTAL_FACING; + + public PackModelEntityBlock(BlockBehaviour.Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(HORIZONTAL_FACING, Direction.NORTH)); + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new PackModelBlockEntity(pos, state); + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + protected void spawnDestroyParticles(Level level, Player player, BlockPos pos, BlockState state) { + // This block is rendered by ItemDisplay instead of block model, + // so vanilla block-break particles would resolve to missing textures. + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(HORIZONTAL_FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(HORIZONTAL_FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(HORIZONTAL_FACING, rotation.rotate(state.getValue(HORIZONTAL_FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(HORIZONTAL_FACING))); + } + + @Override + protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, + Player player, + BlockHitResult hitResult) { + if (level.isClientSide()) { + return net.minecraft.world.InteractionResult.SUCCESS; + } + + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe) { + modelBe.cycleMode(player); + return net.minecraft.world.InteractionResult.SUCCESS; + } + return net.minecraft.world.InteractionResult.PASS; + } + + @Override + protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, + Player player, + InteractionHand hand, BlockHitResult hitResult) { + if (stack.is(Items.PAPER)) { + if (level.isClientSide()) { + return ItemInteractionResult.SUCCESS; + } + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe) { + modelBe.copyConfig(player); + return ItemInteractionResult.SUCCESS; + } + return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + } + + if (stack.is(Items.BOOK)) { + if (level.isClientSide()) { + return ItemInteractionResult.SUCCESS; + } + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe + && level instanceof net.minecraft.server.level.ServerLevel serverLevel) { + modelBe.pasteConfig(serverLevel, pos, state, player); + return ItemInteractionResult.SUCCESS; + } + return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + } + + int direction; + if (stack.is(Items.STICK)) { + direction = 1; + } else if (stack.is(Items.BLAZE_ROD)) { + direction = -1; + } else { + return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + } + + if (level.isClientSide()) { + return ItemInteractionResult.SUCCESS; + } + + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe + && level instanceof net.minecraft.server.level.ServerLevel serverLevel) { + modelBe.adjustCurrentMode(serverLevel, pos, state, direction, player); + return ItemInteractionResult.SUCCESS; + } + + return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + } + + + @Override + @SuppressWarnings("unchecked") + public BlockEntityTicker getTicker(Level level, BlockState state, + BlockEntityType blockEntityType) { + if (level.isClientSide()) { + return null; + } + + BlockEntityType expectedType = com.box3lab.register.modelbe.PackModelBlockEntityRegistrar + .typeFor(state.getBlock()); + if (blockEntityType != expectedType) { + return null; + } + + return (lvl, pos, blockState, be) -> PackModelBlockEntity.serverTick( + lvl, + pos, + blockState, + (PackModelBlockEntity) be); + } +} diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/item/ModelDestroyerItem.java b/Fabric-1.21.1/src/main/java/com/box3lab/item/ModelDestroyerItem.java deleted file mode 100644 index b70e5de..0000000 --- a/Fabric-1.21.1/src/main/java/com/box3lab/item/ModelDestroyerItem.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.box3lab.item; - -import java.util.List; - -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.Display; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; - -public class ModelDestroyerItem extends Item { - private static final double RANGE = 10.0; - - public ModelDestroyerItem(Properties properties) { - super(properties); - } - - @Override - public InteractionResult useOn(UseOnContext context) { - Level level = context.getLevel(); - if (!(level instanceof ServerLevel serverLevel)) { - return InteractionResult.SUCCESS; - } - - Player player = context.getPlayer(); - if (player == null) { - return InteractionResult.PASS; - } - - boolean deleted = deleteTarget(serverLevel, player); - return deleted ? InteractionResult.SUCCESS : InteractionResult.PASS; - } - - private static boolean deleteTarget(ServerLevel level, Player player) { - Display.ItemDisplay target = findDisplay(level, player, RANGE); - if (target == null) { - return false; - } - target.discard(); - return true; - } - - private static Display.ItemDisplay findDisplay(ServerLevel level, Player player, double range) { - Vec3 start = player.getEyePosition(1.0f); - Vec3 look = player.getViewVector(1.0f); - Vec3 end = start.add(look.scale(range)); - AABB searchBox = player.getBoundingBox().expandTowards(look.scale(range)).inflate(1.5); - - List displays = level.getEntitiesOfClass(Display.ItemDisplay.class, searchBox); - Display.ItemDisplay closest = null; - double bestDist = range * range; - - for (Display.ItemDisplay display : displays) { - AABB box = display.getBoundingBox().inflate(0.8); - var hit = box.clip(start, end); - if (hit.isEmpty()) { - continue; - } - double dist = start.distanceToSqr(hit.get()); - if (dist < bestDist) { - bestDist = dist; - closest = display; - } - } - - return closest; - } -} diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/item/ModelDisplayItem.java b/Fabric-1.21.1/src/main/java/com/box3lab/item/ModelDisplayItem.java deleted file mode 100644 index 7e2d5ca..0000000 --- a/Fabric-1.21.1/src/main/java/com/box3lab/item/ModelDisplayItem.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.box3lab.item; - -import net.minecraft.core.BlockPos; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.Display; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.Vec3; - -public class ModelDisplayItem extends Item { - public ModelDisplayItem(Properties properties) { - super(properties); - } - - @Override - public InteractionResult useOn(UseOnContext context) { - Level level = context.getLevel(); - if (!(level instanceof ServerLevel serverLevel)) { - return InteractionResult.SUCCESS; - } - - Display.ItemDisplay display = (Display.ItemDisplay) EntityType.ITEM_DISPLAY.create(serverLevel); - if (display == null) { - return InteractionResult.FAIL; - } - - BlockPos placePos = context.getClickedPos().relative(context.getClickedFace()); - Vec3 pos = Vec3.atCenterOf(placePos); - display.setPos(pos.x, pos.y, pos.z); - - Player player = context.getPlayer(); - if (player != null) { - display.setYRot(player.getYRot()); - } - - ItemStack displayStack = context.getItemInHand().copyWithCount(1); - display.getSlot(0).set(displayStack); - - display.setNoGravity(true); - serverLevel.addFreshEntity(display); - - if (player == null || !player.getAbilities().instabuild) { - context.getItemInHand().shrink(1); - } - - return InteractionResult.SUCCESS; - } -} diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/register/ModBlocks.java b/Fabric-1.21.1/src/main/java/com/box3lab/register/ModBlocks.java index f815bbb..2853bc1 100644 --- a/Fabric-1.21.1/src/main/java/com/box3lab/register/ModBlocks.java +++ b/Fabric-1.21.1/src/main/java/com/box3lab/register/ModBlocks.java @@ -6,6 +6,7 @@ import com.box3lab.Box3; import com.box3lab.register.core.BlockRegistrar; import com.box3lab.register.creative.CreativeTabRegistrar; +import com.box3lab.register.modelbe.PackModelBlockEntityRegistrar; import com.box3lab.register.sound.CategorySoundTypes; import com.box3lab.register.voxel.VoxelBlockFactories; import com.box3lab.register.voxel.VoxelBlockPropertiesFactory; @@ -62,6 +63,7 @@ public static void initialize() { } CreativeTabRegistrar.registerCreativeTabs(Box3.MOD_ID, BLOCKS, data); + PackModelBlockEntityRegistrar.registerAll(); } -} \ No newline at end of file +} diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/register/ModItems.java b/Fabric-1.21.1/src/main/java/com/box3lab/register/ModItems.java index 798e513..f8f2634 100644 --- a/Fabric-1.21.1/src/main/java/com/box3lab/register/ModItems.java +++ b/Fabric-1.21.1/src/main/java/com/box3lab/register/ModItems.java @@ -6,9 +6,6 @@ private ModItems() { } public static void initialize() { - // Register all model-based display items discovered from resource packs - ModelItemRegistrar.registerAll(); - - ModelToolRegistrar.registerAll(); + // Reserved for future item registrations. } } diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/register/ModelItemRegistrar.java b/Fabric-1.21.1/src/main/java/com/box3lab/register/ModelItemRegistrar.java deleted file mode 100644 index 146e975..0000000 --- a/Fabric-1.21.1/src/main/java/com/box3lab/register/ModelItemRegistrar.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.box3lab.register; - -import java.io.File; -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Enumeration; -import java.util.LinkedHashSet; -import java.util.Locale; -import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import com.box3lab.Box3; -import com.box3lab.item.ModelDisplayItem; -import com.box3lab.register.creative.CreativeTabExtras; -import com.box3lab.register.creative.CreativeTabRegistrar; - -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; - -public final class ModelItemRegistrar { - private static final String ITEMS_DIR_PREFIX = "assets/" + Box3.MOD_ID + "/items/"; - public static final String DEFAULT_TAB = "models"; - - private ModelItemRegistrar() { - } - - public static void registerAll() { - Set itemPaths = discoverModelItemPaths(); - if (itemPaths.isEmpty()) { - return; - } - - for (String path : itemPaths) { - ResourceLocation id; - try { - id = ResourceLocation.fromNamespaceAndPath(Box3.MOD_ID, path); - } catch (IllegalArgumentException e) { - continue; - } - - if (BuiltInRegistries.ITEM.containsKey(id)) { - continue; - } - - Item item = new ModelDisplayItem(new Item.Properties()); - Registry.register(BuiltInRegistries.ITEM, id, item); - CreativeTabExtras.add(DEFAULT_TAB, item); - } - - CreativeTabRegistrar.registerModelTab(Box3.MOD_ID); - } - - private static Set discoverModelItemPaths() { - Set results = new LinkedHashSet<>(); - Path resourcepacksDir = FabricLoader.getInstance().getGameDir().resolve("resourcepacks"); - if (!Files.isDirectory(resourcepacksDir)) { - return results; - } - - try (DirectoryStream stream = Files.newDirectoryStream(resourcepacksDir)) { - for (Path entry : stream) { - if (Files.isDirectory(entry)) { - scanDirectoryPack(entry, results); - } else if (isArchive(entry)) { - scanZipPack(entry, results); - } - } - } catch (IOException ignored) { - } - - return results; - } - - private static boolean isArchive(Path path) { - String name = path.getFileName().toString().toLowerCase(Locale.ROOT); - return name.endsWith(".zip") || name.endsWith(".jar"); - } - - private static void scanDirectoryPack(Path packDir, Set out) { - Path itemsDir = packDir.resolve("assets").resolve(Box3.MOD_ID).resolve("items"); - if (!Files.isDirectory(itemsDir)) { - return; - } - - try (var paths = Files.walk(itemsDir)) { - paths.filter(Files::isRegularFile) - .forEach(file -> { - String name = file.getFileName().toString(); - if (!name.endsWith(".json")) { - return; - } - - String rel = itemsDir.relativize(file).toString().replace(File.separatorChar, '/'); - if (rel.endsWith(".json")) { - rel = rel.substring(0, rel.length() - 5); - } - if (!rel.isBlank()) { - out.add(rel); - } - }); - } catch (IOException ignored) { - } - } - - private static void scanZipPack(Path zipPath, Set out) { - try (ZipFile zip = new ZipFile(zipPath.toFile())) { - Enumeration entries = zip.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (entry.isDirectory()) { - continue; - } - String name = entry.getName(); - if (!name.startsWith(ITEMS_DIR_PREFIX) || !name.endsWith(".json")) { - continue; - } - String rel = name.substring(ITEMS_DIR_PREFIX.length(), name.length() - 5); - if (!rel.isBlank()) { - out.add(rel); - } - } - } catch (IOException ignored) { - } - } -} diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/register/ModelToolRegistrar.java b/Fabric-1.21.1/src/main/java/com/box3lab/register/ModelToolRegistrar.java deleted file mode 100644 index 7ff2752..0000000 --- a/Fabric-1.21.1/src/main/java/com/box3lab/register/ModelToolRegistrar.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.box3lab.register; - -import com.box3lab.Box3; -import com.box3lab.item.ModelDestroyerItem; -import com.box3lab.register.creative.CreativeTabExtras; - -import net.fabricmc.fabric.api.event.player.UseEntityCallback; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.resources.ResourceKey; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.Display; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; - -public final class ModelToolRegistrar { - private ModelToolRegistrar() { - } - - public static void registerAll() { - registerDestroyer(); - registerDestroyerHandler(); - } - - private static void registerDestroyer() { - ResourceLocation id = ResourceLocation.fromNamespaceAndPath(Box3.MOD_ID, "model_destroyer"); - if (BuiltInRegistries.ITEM.containsKey(id)) { - return; - } - - Item item = new ModelDestroyerItem(new Item.Properties().stacksTo(1)); - Registry.register(BuiltInRegistries.ITEM, id, item); - CreativeTabExtras.add(ModelItemRegistrar.DEFAULT_TAB, item); - } - - private static void registerDestroyerHandler() { - UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { - ItemStack stack = player.getItemInHand(hand); - if (!(stack.getItem() instanceof ModelDestroyerItem)) { - return InteractionResult.PASS; - } - - if (!(entity instanceof Display.ItemDisplay display)) { - return InteractionResult.PASS; - } - - if (!(world instanceof ServerLevel)) { - return InteractionResult.SUCCESS; - } - - display.discard(); - return InteractionResult.SUCCESS; - }); - } -} diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java b/Fabric-1.21.1/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java index 2eb5a22..fb1a481 100644 --- a/Fabric-1.21.1/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java +++ b/Fabric-1.21.1/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java @@ -13,17 +13,17 @@ import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; -import net.minecraft.world.level.block.Block; import net.minecraft.world.level.ItemLike; - -import static com.box3lab.register.ModelItemRegistrar.DEFAULT_TAB; +import net.minecraft.world.level.block.Block; public final class CreativeTabRegistrar { + public static final String DEFAULT_MODEL_TAB = "models"; + private CreativeTabRegistrar() { } @@ -96,7 +96,7 @@ public static void registerCreativeTabs(String modId, Map blocks, } public static void registerModelTab(String modId) { - String categoryPath = sanitizeCategoryPath(DEFAULT_TAB); + String categoryPath = sanitizeCategoryPath(DEFAULT_MODEL_TAB); if (categoryPath.isBlank()) { return; } diff --git a/Fabric-1.21.1/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java b/Fabric-1.21.1/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java new file mode 100644 index 0000000..2aa561c --- /dev/null +++ b/Fabric-1.21.1/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java @@ -0,0 +1,219 @@ +package com.box3lab.register.modelbe; + +import static com.box3lab.Box3.MOD_ID; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import com.box3lab.block.entity.PackModelEntityBlock; +import com.box3lab.register.creative.CreativeTabExtras; +import com.box3lab.register.creative.CreativeTabRegistrar; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.MapColor; + +public final class PackModelBlockEntityRegistrar { + private static final String ASSET_PREFIX = "assets/" + MOD_ID + "/"; + + private static final Map> TYPES_BY_BLOCK = new LinkedHashMap<>(); + + private PackModelBlockEntityRegistrar() { + } + + public static void registerAll() { + Set modelNames = discoverPairedModelNames(); + if (modelNames.isEmpty()) { + return; + } + + for (String name : modelNames) { + ResourceLocation id = ResourceLocation.tryBuild(MOD_ID, name); + if (id == null) { + continue; + } + + ResourceKey blockKey = ResourceKey.create(Registries.BLOCK, id); + if (BuiltInRegistries.BLOCK.containsKey(blockKey)) { + continue; + } + + Block block = new PackModelEntityBlock( + BlockBehaviour.Properties.of() + .mapColor(MapColor.STONE) + .strength(1.5F, 6.0F) + .noOcclusion() + .isViewBlocking((state, level, pos) -> false) + .isSuffocating((state, level, pos) -> false) + .isRedstoneConductor((state, level, pos) -> false)); + Registry.register(BuiltInRegistries.BLOCK, blockKey, block); + + ResourceKey itemKey = ResourceKey.create( + Registries.ITEM, + ResourceLocation.fromNamespaceAndPath(MOD_ID, name)); + if (!BuiltInRegistries.ITEM.containsKey(itemKey)) { + final String itemTranslationKey = "item." + MOD_ID + "." + name; + Item item = new BlockItem(block, new Item.Properties()) { + @Override + public Component getName(ItemStack stack) { + return Component.translatable(itemTranslationKey); + } + }; + Registry.register(BuiltInRegistries.ITEM, itemKey, item); + CreativeTabExtras.add(CreativeTabRegistrar.DEFAULT_MODEL_TAB, item); + } + + ResourceKey> blockEntityKey = ResourceKey.create(Registries.BLOCK_ENTITY_TYPE, id); + if (BuiltInRegistries.BLOCK_ENTITY_TYPE.containsKey(blockEntityKey)) { + continue; + } + + BlockEntityType type = FabricBlockEntityTypeBuilder + .create(com.box3lab.block.entity.PackModelBlockEntity::new, block) + .build(); + + Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE, blockEntityKey, type); + TYPES_BY_BLOCK.put(block, type); + } + + CreativeTabRegistrar.registerModelTab(MOD_ID); + } + + public static BlockEntityType typeFor(Block block) { + BlockEntityType type = TYPES_BY_BLOCK.get(block); + if (type == null) { + throw new IllegalStateException("No block entity type bound for block: " + block); + } + return type; + } + + private static Set discoverPairedModelNames() { + Set result = new LinkedHashSet<>(); + Path packsRoot = FabricLoader.getInstance().getGameDir().resolve("resourcepacks"); + if (!Files.isDirectory(packsRoot)) { + return result; + } + + try (var entries = Files.list(packsRoot)) { + entries.forEach(entry -> { + if (Files.isDirectory(entry)) { + collectFromDirectory(entry, result); + } else if (isArchive(entry)) { + collectFromArchive(entry, result); + } + }); + } catch (IOException ignored) { + } + + return result; + } + + private static void collectFromDirectory(Path packDir, Set out) { + Path assetsRoot = packDir.resolve("assets").resolve(MOD_ID); + if (!Files.isDirectory(assetsRoot)) { + return; + } + + Set models = collectBaseNamesFromDirectory(assetsRoot, ".json"); + if (models.isEmpty()) { + return; + } + + Set textures = collectBaseNamesFromDirectory(assetsRoot, ".png"); + if (textures.isEmpty()) { + return; + } + + for (String model : models) { + if (textures.contains(model)) { + out.add(model); + } + } + } + + private static Set collectBaseNamesFromDirectory(Path root, String suffix) { + Set names = new LinkedHashSet<>(); + try (var files = Files.walk(root)) { + files.filter(Files::isRegularFile).forEach(path -> { + String fileName = path.getFileName().toString(); + if (!fileName.toLowerCase(Locale.ROOT).endsWith(suffix)) { + return; + } + + String base = fileName.substring(0, fileName.length() - suffix.length()).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + names.add(base); + } + }); + } catch (IOException ignored) { + } + return names; + } + + private static void collectFromArchive(Path archive, Set out) { + try (ZipFile zip = new ZipFile(archive.toFile())) { + Set models = new LinkedHashSet<>(); + Set textures = new LinkedHashSet<>(); + + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + continue; + } + + String name = entry.getName(); + if (!name.startsWith(ASSET_PREFIX)) { + continue; + } + + String fileName = name.substring(name.lastIndexOf('/') + 1); + String lower = fileName.toLowerCase(Locale.ROOT); + if (lower.endsWith(".json")) { + String base = fileName.substring(0, fileName.length() - 5).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + models.add(base); + } + } else if (lower.endsWith(".png")) { + String base = fileName.substring(0, fileName.length() - 4).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + textures.add(base); + } + } + } + + for (String model : models) { + if (textures.contains(model)) { + out.add(model); + } + } + } catch (IOException ignored) { + } + } + + private static boolean isArchive(Path path) { + String name = path.getFileName().toString().toLowerCase(Locale.ROOT); + return name.endsWith(".zip") || name.endsWith(".jar"); + } +} diff --git a/Fabric-1.21.1/src/main/resources/assets/box3/lang/en_us.json b/Fabric-1.21.1/src/main/resources/assets/box3/lang/en_us.json index 9b34656..00c4e98 100644 --- a/Fabric-1.21.1/src/main/resources/assets/box3/lang/en_us.json +++ b/Fabric-1.21.1/src/main/resources/assets/box3/lang/en_us.json @@ -411,6 +411,15 @@ "command.box3.box3barrier.status": "Barrier visible: %s", "command.box3.box3barrier.set": "Barrier visibility set to: %s", "command.box3.box3barrier.toggled": "Barrier visibility toggled to: %s (re-enter the world to fully apply)", - "item.box3.model_destroyer": "Model destruction bucket", + "message.box3.model.config.mode": "Model config mode: %s (Stick: +, Blaze Rod: -, Paper: copy, Book: paste)", + "message.box3.model.config.status": "mode=%s scale=%s offset=(%s, %s, %s) rot=%s", + "message.box3.model.config.mode.scale": "Scale", + "message.box3.model.config.mode.offset_x": "Offset X", + "message.box3.model.config.mode.offset_y": "Offset Y", + "message.box3.model.config.mode.offset_z": "Offset Z", + "message.box3.model.config.mode.rotation": "Rotation", + "message.box3.model.config.copy.success": "Copied current model config.", + "message.box3.model.config.copy.empty": "No copied config found.", + "message.box3.model.config.copy.pasted": "Pasted copied model config.", "flat_world_preset.box3.box3_plains_world": "Box3 Plains" } diff --git a/Fabric-1.21.1/src/main/resources/assets/box3/lang/zh_cn.json b/Fabric-1.21.1/src/main/resources/assets/box3/lang/zh_cn.json index 29bebcc..82b8d1e 100644 --- a/Fabric-1.21.1/src/main/resources/assets/box3/lang/zh_cn.json +++ b/Fabric-1.21.1/src/main/resources/assets/box3/lang/zh_cn.json @@ -399,6 +399,15 @@ "command.box3.box3barrier.status": "屏障可见状态:%s", "command.box3.box3barrier.set": "屏障可见状态已设置为:%s", "command.box3.box3barrier.toggled": "屏障可见状态已切换为:%s(重新进入世界以完全生效)", - "item.box3.model_destroyer": "模型销毁桶", + "message.box3.model.config.mode": "模型配置模式:%s(木棍:增加,烈焰棒:减少,纸:复制,书:粘贴)", + "message.box3.model.config.status": "模式=%s 缩放=%s 偏移=(%s, %s, %s) 旋转=%s", + "message.box3.model.config.mode.scale": "缩放", + "message.box3.model.config.mode.offset_x": "X偏移", + "message.box3.model.config.mode.offset_y": "Y偏移", + "message.box3.model.config.mode.offset_z": "Z偏移", + "message.box3.model.config.mode.rotation": "旋转", + "message.box3.model.config.copy.success": "已复制当前模型参数。", + "message.box3.model.config.copy.empty": "没有可粘贴的已复制参数。", + "message.box3.model.config.copy.pasted": "已粘贴模型参数。", "flat_world_preset.box3.box3_plains_world": "神岛平原" } diff --git a/Fabric-1.21.11/gradle.properties b/Fabric-1.21.11/gradle.properties index 8b57103..1d62dd8 100644 --- a/Fabric-1.21.11/gradle.properties +++ b/Fabric-1.21.11/gradle.properties @@ -12,7 +12,7 @@ loader_version=0.18.4 loom_version=1.15-SNAPSHOT # Mod Properties -mod_version=1.4.1-mc1.21.11 +mod_version=1.4.2-mc1.21.11 maven_group=com.box3lab archives_base_name=box3 diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java b/Fabric-1.21.11/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java new file mode 100644 index 0000000..df361f1 --- /dev/null +++ b/Fabric-1.21.11/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java @@ -0,0 +1,269 @@ +package com.box3lab.block.entity; + +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import com.box3lab.register.modelbe.PackModelBlockEntityRegistrar; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.permissions.PermissionSet; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Display; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +public class PackModelBlockEntity extends BlockEntity { + private static final long RESPAWN_INTERVAL_TICKS = 20L; + private static final String DISPLAY_TAG_PREFIX = "box3_pack_model:"; + + private static final float SCALE_STEP = 0.1F; + private static final float SCALE_MIN = 0.1F; + private static final float SCALE_MAX = 4.0F; + private static final float OFFSET_STEP = 0.05F; + private static final float ROTATION_STEP = 15.0F; + private static final Map CONFIG_CLIPBOARD = new ConcurrentHashMap<>(); + + private float scale = 1.0F; + private float offsetX = 0.0F; + private float offsetY = 0.0F; + private float offsetZ = 0.0F; + private float rotationOffset = 0.0F; + private int modeIndex = 0; + + public PackModelBlockEntity(BlockPos pos, BlockState state) { + super(PackModelBlockEntityRegistrar.typeFor(state.getBlock()), pos, state); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.scale = clamp(input.getFloatOr("scale", 1.0F), SCALE_MIN, SCALE_MAX); + this.offsetX = input.getFloatOr("offset_x", 0.0F); + this.offsetY = input.getFloatOr("offset_y", 0.0F); + this.offsetZ = input.getFloatOr("offset_z", 0.0F); + this.rotationOffset = input.getFloatOr("rotation_offset", 0.0F); + this.modeIndex = Math.floorMod(input.getIntOr("config_mode", 0), Mode.values().length); + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putFloat("scale", this.scale); + output.putFloat("offset_x", this.offsetX); + output.putFloat("offset_y", this.offsetY); + output.putFloat("offset_z", this.offsetZ); + output.putFloat("rotation_offset", this.rotationOffset); + output.putInt("config_mode", this.modeIndex); + } + + @Override + public void setRemoved() { + removeDisplaysAt(this.level, this.getBlockPos()); + super.setRemoved(); + } + + public static void serverTick(Level level, BlockPos pos, BlockState state, PackModelBlockEntity blockEntity) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + + String tag = displayTag(pos); + var displays = findDisplays(serverLevel, pos, tag); + + if (displays.isEmpty()) { + if (serverLevel.getGameTime() % RESPAWN_INTERVAL_TICKS != 0) { + return; + } + spawnDisplay(serverLevel, pos, state, blockEntity, tag); + return; + } + + for (Display.ItemDisplay display : displays) { + blockEntity.applyPose(serverLevel, pos, state, display); + } + } + + public void cycleMode(net.minecraft.world.entity.player.Player player) { + this.modeIndex = (this.modeIndex + 1) % Mode.values().length; + this.setChanged(); + player.displayClientMessage(Component.translatable( + "message.box3.model.config.mode", + Component.translatable(currentMode().translationKey())), true); + } + + public void adjustCurrentMode(ServerLevel level, BlockPos pos, BlockState state, int direction, + net.minecraft.world.entity.player.Player player) { + Mode mode = currentMode(); + switch (mode) { + case SCALE -> this.scale = clamp(this.scale + SCALE_STEP * direction, SCALE_MIN, SCALE_MAX); + case OFFSET_X -> this.offsetX += OFFSET_STEP * direction; + case OFFSET_Y -> this.offsetY += OFFSET_STEP * direction; + case OFFSET_Z -> this.offsetZ += OFFSET_STEP * direction; + case ROTATION -> this.rotationOffset = normalizeDegrees(this.rotationOffset + ROTATION_STEP * direction); + } + + this.setChanged(); + player.displayClientMessage(statusComponent(), true); + applyToDisplays(level, pos, state); + } + + public void copyConfig(net.minecraft.world.entity.player.Player player) { + CONFIG_CLIPBOARD.put(player.getUUID(), new ConfigSnapshot(this.scale, this.offsetX, this.offsetY, this.offsetZ, this.rotationOffset)); + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.success"), true); + } + + public void pasteConfig(ServerLevel level, BlockPos pos, BlockState state, net.minecraft.world.entity.player.Player player) { + ConfigSnapshot snapshot = CONFIG_CLIPBOARD.get(player.getUUID()); + if (snapshot == null) { + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.empty"), true); + return; + } + + this.scale = clamp(snapshot.scale, SCALE_MIN, SCALE_MAX); + this.offsetX = snapshot.offsetX; + this.offsetY = snapshot.offsetY; + this.offsetZ = snapshot.offsetZ; + this.rotationOffset = normalizeDegrees(snapshot.rotationOffset); + this.setChanged(); + + applyToDisplays(level, pos, state); + player.displayClientMessage(Component.translatable("message.box3.model.config.copy.pasted"), true); + player.displayClientMessage(statusComponent(), true); + } + + public static void removeDisplaysAt(Level level, BlockPos pos) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + + String tag = displayTag(pos); + for (Display.ItemDisplay display : findDisplays(serverLevel, pos, tag)) { + display.discard(); + } + } + + private static void spawnDisplay(ServerLevel level, BlockPos pos, BlockState state, PackModelBlockEntity be, String tag) { + Display.ItemDisplay display = EntityType.ITEM_DISPLAY.create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (display == null) { + return; + } + + display.setNoGravity(true); + display.setInvulnerable(true); + display.getSlot(0).set(new ItemStack(state.getBlock())); + display.addTag(tag); + + be.applyPose(level, pos, state, display); + level.addFreshEntity(display); + } + + private void applyPose(ServerLevel level, BlockPos pos, BlockState state, Display.ItemDisplay display) { + double x = pos.getX() + 0.5D; + double y = pos.getY() + this.offsetY; + double z = pos.getZ() + 0.5D; + display.setPos(x, y, z); + + float baseYaw = 0.0F; + if (state.hasProperty(PackModelEntityBlock.HORIZONTAL_FACING)) { + Direction facing = state.getValue(PackModelEntityBlock.HORIZONTAL_FACING); + baseYaw = facing.toYRot(); + } + display.setYRot(normalizeDegrees(baseYaw + this.rotationOffset)); + + applyDisplayTransformation(level, display); + } + + private void applyDisplayTransformation(ServerLevel level, Display.ItemDisplay display) { + MinecraftServer server = level.getServer(); + CommandSourceStack source = server.createCommandSourceStack().withSuppressedOutput() + .withPermission(PermissionSet.ALL_PERMISSIONS); + + String cmd = String.format( + Locale.ROOT, + "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]}}", + display.getStringUUID(), + fmt(this.offsetX), fmt(0.0F), fmt(this.offsetZ), + fmt(this.scale), fmt(this.scale), fmt(this.scale)); + server.getCommands().performPrefixedCommand(source, cmd); + } + + private static String fmt(float value) { + return String.format(Locale.ROOT, "%.3f", value); + } + + private static java.util.List findDisplays(ServerLevel level, BlockPos pos, String tag) { + return level.getEntitiesOfClass( + Display.ItemDisplay.class, + new AABB(pos).inflate(0.25D), + display -> display.getTags().contains(tag)); + } + + private void applyToDisplays(ServerLevel level, BlockPos pos, BlockState state) { + for (Display.ItemDisplay display : findDisplays(level, pos, displayTag(pos))) { + applyPose(level, pos, state, display); + } + } + + private Mode currentMode() { + return Mode.values()[this.modeIndex]; + } + + private Component statusComponent() { + return Component.translatable( + "message.box3.model.config.status", + Component.translatable(currentMode().translationKey()), + String.format(Locale.ROOT, "%.2f", this.scale), + String.format(Locale.ROOT, "%.2f", this.offsetX), + String.format(Locale.ROOT, "%.2f", this.offsetY), + String.format(Locale.ROOT, "%.2f", this.offsetZ), + String.format(Locale.ROOT, "%.1f", this.rotationOffset)); + } + + private static float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } + + private static float normalizeDegrees(float value) { + float v = value % 360.0F; + return v < 0.0F ? v + 360.0F : v; + } + + private static String displayTag(BlockPos pos) { + return DISPLAY_TAG_PREFIX + pos.asLong(); + } + + private enum Mode { + SCALE("scale"), + OFFSET_X("offset_x"), + OFFSET_Y("offset_y"), + OFFSET_Z("offset_z"), + ROTATION("rotation"); + + private final String keyPart; + + Mode(String keyPart) { + this.keyPart = keyPart; + } + + public String translationKey() { + return "message.box3.model.config.mode." + this.keyPart; + } + } + + private record ConfigSnapshot(float scale, float offsetX, float offsetY, float offsetZ, float rotationOffset) { + } +} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java b/Fabric-1.21.11/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java new file mode 100644 index 0000000..bd449ca --- /dev/null +++ b/Fabric-1.21.11/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java @@ -0,0 +1,156 @@ +package com.box3lab.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; +import org.jspecify.annotations.Nullable; + +public class PackModelEntityBlock extends Block implements EntityBlock { + public static final EnumProperty HORIZONTAL_FACING = BlockStateProperties.HORIZONTAL_FACING; + + public PackModelEntityBlock(BlockBehaviour.Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(HORIZONTAL_FACING, Direction.NORTH)); + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new PackModelBlockEntity(pos, state); + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + protected void spawnDestroyParticles(Level level, Player player, BlockPos pos, BlockState state) { + // This block is rendered by ItemDisplay instead of block model, + // so vanilla block-break particles would resolve to missing textures. + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(HORIZONTAL_FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(HORIZONTAL_FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(HORIZONTAL_FACING, rotation.rotate(state.getValue(HORIZONTAL_FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(HORIZONTAL_FACING))); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, + BlockHitResult hitResult) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe) { + modelBe.cycleMode(player); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + @Override + protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, + InteractionHand hand, BlockHitResult hitResult) { + if (stack.is(Items.PAPER)) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe) { + modelBe.copyConfig(player); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + if (stack.is(Items.BOOK)) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe && level instanceof net.minecraft.server.level.ServerLevel serverLevel) { + modelBe.pasteConfig(serverLevel, pos, state, player); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + int direction; + if (stack.is(Items.STICK)) { + direction = 1; + } else if (stack.is(Items.BLAZE_ROD)) { + direction = -1; + } else { + return InteractionResult.TRY_WITH_EMPTY_HAND; + } + + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe && level instanceof net.minecraft.server.level.ServerLevel serverLevel) { + modelBe.adjustCurrentMode(serverLevel, pos, state, direction, player); + return InteractionResult.SUCCESS; + } + + return InteractionResult.PASS; + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (level.isClientSide()) { + return null; + } + + BlockEntityType expectedType = com.box3lab.register.modelbe.PackModelBlockEntityRegistrar.typeFor(state.getBlock()); + if (blockEntityType != expectedType) { + return null; + } + + return (lvl, pos, blockState, be) -> PackModelBlockEntity.serverTick( + lvl, + pos, + blockState, + (PackModelBlockEntity) be); + } +} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/item/ModelDestroyerItem.java b/Fabric-1.21.11/src/main/java/com/box3lab/item/ModelDestroyerItem.java deleted file mode 100644 index b70e5de..0000000 --- a/Fabric-1.21.11/src/main/java/com/box3lab/item/ModelDestroyerItem.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.box3lab.item; - -import java.util.List; - -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.Display; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; - -public class ModelDestroyerItem extends Item { - private static final double RANGE = 10.0; - - public ModelDestroyerItem(Properties properties) { - super(properties); - } - - @Override - public InteractionResult useOn(UseOnContext context) { - Level level = context.getLevel(); - if (!(level instanceof ServerLevel serverLevel)) { - return InteractionResult.SUCCESS; - } - - Player player = context.getPlayer(); - if (player == null) { - return InteractionResult.PASS; - } - - boolean deleted = deleteTarget(serverLevel, player); - return deleted ? InteractionResult.SUCCESS : InteractionResult.PASS; - } - - private static boolean deleteTarget(ServerLevel level, Player player) { - Display.ItemDisplay target = findDisplay(level, player, RANGE); - if (target == null) { - return false; - } - target.discard(); - return true; - } - - private static Display.ItemDisplay findDisplay(ServerLevel level, Player player, double range) { - Vec3 start = player.getEyePosition(1.0f); - Vec3 look = player.getViewVector(1.0f); - Vec3 end = start.add(look.scale(range)); - AABB searchBox = player.getBoundingBox().expandTowards(look.scale(range)).inflate(1.5); - - List displays = level.getEntitiesOfClass(Display.ItemDisplay.class, searchBox); - Display.ItemDisplay closest = null; - double bestDist = range * range; - - for (Display.ItemDisplay display : displays) { - AABB box = display.getBoundingBox().inflate(0.8); - var hit = box.clip(start, end); - if (hit.isEmpty()) { - continue; - } - double dist = start.distanceToSqr(hit.get()); - if (dist < bestDist) { - bestDist = dist; - closest = display; - } - } - - return closest; - } -} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/item/ModelDisplayItem.java b/Fabric-1.21.11/src/main/java/com/box3lab/item/ModelDisplayItem.java deleted file mode 100644 index 101a34c..0000000 --- a/Fabric-1.21.11/src/main/java/com/box3lab/item/ModelDisplayItem.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.box3lab.item; - -import net.minecraft.core.BlockPos; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.Display; -import net.minecraft.world.entity.EntitySpawnReason; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.Vec3; - -public class ModelDisplayItem extends Item { - public ModelDisplayItem(Properties properties) { - super(properties); - } - - @Override - public InteractionResult useOn(UseOnContext context) { - Level level = context.getLevel(); - if (!(level instanceof ServerLevel serverLevel)) { - return InteractionResult.SUCCESS; - } - - Display.ItemDisplay display = EntityType.ITEM_DISPLAY.create(serverLevel, EntitySpawnReason.SPAWN_ITEM_USE); - if (display == null) { - return InteractionResult.FAIL; - } - - BlockPos placePos = context.getClickedPos().relative(context.getClickedFace()); - Vec3 pos = Vec3.atCenterOf(placePos); - display.setPos(pos.x, pos.y, pos.z); - - Player player = context.getPlayer(); - if (player != null) { - display.setYRot(player.getYRot()); - } - - ItemStack displayStack = context.getItemInHand().copyWithCount(1); - display.getSlot(0).set(displayStack); - - display.setNoGravity(true); - serverLevel.addFreshEntity(display); - - if (player == null || !player.getAbilities().instabuild) { - context.getItemInHand().shrink(1); - } - - return InteractionResult.SUCCESS; - } -} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/register/ModBlocks.java b/Fabric-1.21.11/src/main/java/com/box3lab/register/ModBlocks.java index f815bbb..2853bc1 100644 --- a/Fabric-1.21.11/src/main/java/com/box3lab/register/ModBlocks.java +++ b/Fabric-1.21.11/src/main/java/com/box3lab/register/ModBlocks.java @@ -6,6 +6,7 @@ import com.box3lab.Box3; import com.box3lab.register.core.BlockRegistrar; import com.box3lab.register.creative.CreativeTabRegistrar; +import com.box3lab.register.modelbe.PackModelBlockEntityRegistrar; import com.box3lab.register.sound.CategorySoundTypes; import com.box3lab.register.voxel.VoxelBlockFactories; import com.box3lab.register.voxel.VoxelBlockPropertiesFactory; @@ -62,6 +63,7 @@ public static void initialize() { } CreativeTabRegistrar.registerCreativeTabs(Box3.MOD_ID, BLOCKS, data); + PackModelBlockEntityRegistrar.registerAll(); } -} \ No newline at end of file +} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/register/ModItems.java b/Fabric-1.21.11/src/main/java/com/box3lab/register/ModItems.java index 798e513..f8f2634 100644 --- a/Fabric-1.21.11/src/main/java/com/box3lab/register/ModItems.java +++ b/Fabric-1.21.11/src/main/java/com/box3lab/register/ModItems.java @@ -6,9 +6,6 @@ private ModItems() { } public static void initialize() { - // Register all model-based display items discovered from resource packs - ModelItemRegistrar.registerAll(); - - ModelToolRegistrar.registerAll(); + // Reserved for future item registrations. } } diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/register/ModelItemRegistrar.java b/Fabric-1.21.11/src/main/java/com/box3lab/register/ModelItemRegistrar.java deleted file mode 100644 index facc38c..0000000 --- a/Fabric-1.21.11/src/main/java/com/box3lab/register/ModelItemRegistrar.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.box3lab.register; - -import java.io.File; -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Enumeration; -import java.util.LinkedHashSet; -import java.util.Locale; -import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import com.box3lab.Box3; -import com.box3lab.item.ModelDisplayItem; -import com.box3lab.register.creative.CreativeTabExtras; -import com.box3lab.register.creative.CreativeTabRegistrar; - -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.resources.Identifier; -import net.minecraft.resources.ResourceKey; -import net.minecraft.world.item.Item; - -public final class ModelItemRegistrar { - private static final String ITEMS_DIR_PREFIX = "assets/" + Box3.MOD_ID + "/items/"; - public static final String DEFAULT_TAB = "models"; - - private ModelItemRegistrar() { - } - - public static void registerAll() { - Set itemPaths = discoverModelItemPaths(); - if (itemPaths.isEmpty()) { - return; - } - - for (String path : itemPaths) { - Identifier id = Identifier.tryBuild(Box3.MOD_ID, path); - if (id == null) { - continue; - } - - ResourceKey key = ResourceKey.create(Registries.ITEM, id); - if (BuiltInRegistries.ITEM.containsKey(key)) { - continue; - } - - Item item = new ModelDisplayItem(new Item.Properties().setId(key)); - Registry.register(BuiltInRegistries.ITEM, key, item); - CreativeTabExtras.add(DEFAULT_TAB, item); - } - - CreativeTabRegistrar.registerModelTab(Box3.MOD_ID); - } - - private static Set discoverModelItemPaths() { - Set results = new LinkedHashSet<>(); - Path resourcepacksDir = FabricLoader.getInstance().getGameDir().resolve("resourcepacks"); - if (!Files.isDirectory(resourcepacksDir)) { - return results; - } - - try (DirectoryStream stream = Files.newDirectoryStream(resourcepacksDir)) { - for (Path entry : stream) { - if (Files.isDirectory(entry)) { - scanDirectoryPack(entry, results); - } else if (isArchive(entry)) { - scanZipPack(entry, results); - } - } - } catch (IOException ignored) { - } - - return results; - } - - private static boolean isArchive(Path path) { - String name = path.getFileName().toString().toLowerCase(Locale.ROOT); - return name.endsWith(".zip") || name.endsWith(".jar"); - } - - private static void scanDirectoryPack(Path packDir, Set out) { - Path itemsDir = packDir.resolve("assets").resolve(Box3.MOD_ID).resolve("items"); - if (!Files.isDirectory(itemsDir)) { - return; - } - - try (var paths = Files.walk(itemsDir)) { - paths.filter(Files::isRegularFile) - .forEach(file -> { - String name = file.getFileName().toString(); - if (!name.endsWith(".json")) { - return; - } - - String rel = itemsDir.relativize(file).toString().replace(File.separatorChar, '/'); - if (rel.endsWith(".json")) { - rel = rel.substring(0, rel.length() - 5); - } - if (!rel.isBlank()) { - out.add(rel); - } - }); - } catch (IOException ignored) { - } - } - - private static void scanZipPack(Path zipPath, Set out) { - try (ZipFile zip = new ZipFile(zipPath.toFile())) { - Enumeration entries = zip.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (entry.isDirectory()) { - continue; - } - String name = entry.getName(); - if (!name.startsWith(ITEMS_DIR_PREFIX) || !name.endsWith(".json")) { - continue; - } - String rel = name.substring(ITEMS_DIR_PREFIX.length(), name.length() - 5); - if (!rel.isBlank()) { - out.add(rel); - } - } - } catch (IOException ignored) { - } - } -} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/register/ModelToolRegistrar.java b/Fabric-1.21.11/src/main/java/com/box3lab/register/ModelToolRegistrar.java deleted file mode 100644 index b7dc95c..0000000 --- a/Fabric-1.21.11/src/main/java/com/box3lab/register/ModelToolRegistrar.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.box3lab.register; - -import com.box3lab.Box3; -import com.box3lab.item.ModelDestroyerItem; -import com.box3lab.register.creative.CreativeTabExtras; - -import net.fabricmc.fabric.api.event.player.UseEntityCallback; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.resources.Identifier; -import net.minecraft.resources.ResourceKey; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.Display; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; - -public final class ModelToolRegistrar { - private ModelToolRegistrar() { - } - - public static void registerAll() { - registerDestroyer(); - registerDestroyerHandler(); - } - - private static void registerDestroyer() { - Identifier id = Identifier.fromNamespaceAndPath(Box3.MOD_ID, "model_destroyer"); - ResourceKey key = ResourceKey.create(Registries.ITEM, id); - if (BuiltInRegistries.ITEM.containsKey(key)) { - return; - } - - Item item = new ModelDestroyerItem(new Item.Properties().setId(key).stacksTo(1)); - Registry.register(BuiltInRegistries.ITEM, key, item); - CreativeTabExtras.add(ModelItemRegistrar.DEFAULT_TAB, item); - } - - private static void registerDestroyerHandler() { - UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { - ItemStack stack = player.getItemInHand(hand); - if (!(stack.getItem() instanceof ModelDestroyerItem)) { - return InteractionResult.PASS; - } - - if (!(entity instanceof Display.ItemDisplay display)) { - return InteractionResult.PASS; - } - - if (!(world instanceof ServerLevel)) { - return InteractionResult.SUCCESS; - } - - display.discard(); - return InteractionResult.SUCCESS; - }); - } -} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java b/Fabric-1.21.11/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java index 89d5645..4449772 100644 --- a/Fabric-1.21.11/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java +++ b/Fabric-1.21.11/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java @@ -21,9 +21,9 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.ItemLike; -import static com.box3lab.register.ModelItemRegistrar.DEFAULT_TAB; - public final class CreativeTabRegistrar { + public static final String DEFAULT_MODEL_TAB = "models"; + private CreativeTabRegistrar() { } @@ -96,7 +96,7 @@ public static void registerCreativeTabs(String modId, Map blocks, } public static void registerModelTab(String modId) { - String categoryPath = sanitizeCategoryPath(DEFAULT_TAB); + String categoryPath = sanitizeCategoryPath(DEFAULT_MODEL_TAB); if (categoryPath.isBlank()) { return; } diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java b/Fabric-1.21.11/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java new file mode 100644 index 0000000..04e3625 --- /dev/null +++ b/Fabric-1.21.11/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java @@ -0,0 +1,212 @@ +package com.box3lab.register.modelbe; + +import static com.box3lab.Box3.MOD_ID; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import com.box3lab.block.entity.PackModelEntityBlock; +import com.box3lab.register.creative.CreativeTabExtras; +import com.box3lab.register.creative.CreativeTabRegistrar; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.MapColor; + +public final class PackModelBlockEntityRegistrar { + private static final String ASSET_PREFIX = "assets/" + MOD_ID + "/"; + + private static final Map> TYPES_BY_BLOCK = new LinkedHashMap<>(); + + private PackModelBlockEntityRegistrar() { + } + + public static void registerAll() { + Set modelNames = discoverPairedModelNames(); + if (modelNames.isEmpty()) { + return; + } + + for (String name : modelNames) { + Identifier id = Identifier.tryBuild(MOD_ID, name); + if (id == null) { + continue; + } + + ResourceKey blockKey = ResourceKey.create(Registries.BLOCK, id); + if (BuiltInRegistries.BLOCK.containsKey(blockKey)) { + continue; + } + + Block block = new PackModelEntityBlock( + BlockBehaviour.Properties.of() + .setId(blockKey) + .mapColor(MapColor.STONE) + .strength(1.5F, 6.0F) + .noOcclusion() + .isViewBlocking((state, level, pos) -> false) + .isSuffocating((state, level, pos) -> false) + .isRedstoneConductor((state, level, pos) -> false)); + Registry.register(BuiltInRegistries.BLOCK, blockKey, block); + + ResourceKey itemKey = ResourceKey.create( + Registries.ITEM, + Identifier.fromNamespaceAndPath(MOD_ID, name)); + if (!BuiltInRegistries.ITEM.containsKey(itemKey)) { + Item item = new BlockItem(block, new Item.Properties().setId(itemKey).useItemDescriptionPrefix()); + Registry.register(BuiltInRegistries.ITEM, itemKey, item); + CreativeTabExtras.add(CreativeTabRegistrar.DEFAULT_MODEL_TAB, item); + } + + ResourceKey> blockEntityKey = ResourceKey.create(Registries.BLOCK_ENTITY_TYPE, id); + if (BuiltInRegistries.BLOCK_ENTITY_TYPE.containsKey(blockEntityKey)) { + continue; + } + + BlockEntityType type = FabricBlockEntityTypeBuilder + .create(com.box3lab.block.entity.PackModelBlockEntity::new, block) + .build(); + + Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE, blockEntityKey, type); + TYPES_BY_BLOCK.put(block, type); + } + + CreativeTabRegistrar.registerModelTab(MOD_ID); + } + + public static BlockEntityType typeFor(Block block) { + BlockEntityType type = TYPES_BY_BLOCK.get(block); + if (type == null) { + throw new IllegalStateException("No block entity type bound for block: " + block); + } + return type; + } + + private static Set discoverPairedModelNames() { + Set result = new LinkedHashSet<>(); + Path packsRoot = FabricLoader.getInstance().getGameDir().resolve("resourcepacks"); + if (!Files.isDirectory(packsRoot)) { + return result; + } + + try (var entries = Files.list(packsRoot)) { + entries.forEach(entry -> { + if (Files.isDirectory(entry)) { + collectFromDirectory(entry, result); + } else if (isArchive(entry)) { + collectFromArchive(entry, result); + } + }); + } catch (IOException ignored) { + } + + return result; + } + + private static void collectFromDirectory(Path packDir, Set out) { + Path assetsRoot = packDir.resolve("assets").resolve(MOD_ID); + if (!Files.isDirectory(assetsRoot)) { + return; + } + + Set models = collectBaseNamesFromDirectory(assetsRoot, ".json"); + if (models.isEmpty()) { + return; + } + + Set textures = collectBaseNamesFromDirectory(assetsRoot, ".png"); + if (textures.isEmpty()) { + return; + } + + for (String model : models) { + if (textures.contains(model)) { + out.add(model); + } + } + } + + private static Set collectBaseNamesFromDirectory(Path root, String suffix) { + Set names = new LinkedHashSet<>(); + try (var files = Files.walk(root)) { + files.filter(Files::isRegularFile).forEach(path -> { + String fileName = path.getFileName().toString(); + if (!fileName.toLowerCase(Locale.ROOT).endsWith(suffix)) { + return; + } + + String base = fileName.substring(0, fileName.length() - suffix.length()).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + names.add(base); + } + }); + } catch (IOException ignored) { + } + return names; + } + + private static void collectFromArchive(Path archive, Set out) { + try (ZipFile zip = new ZipFile(archive.toFile())) { + Set models = new LinkedHashSet<>(); + Set textures = new LinkedHashSet<>(); + + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + continue; + } + + String name = entry.getName(); + if (!name.startsWith(ASSET_PREFIX)) { + continue; + } + + String fileName = name.substring(name.lastIndexOf('/') + 1); + String lower = fileName.toLowerCase(Locale.ROOT); + if (lower.endsWith(".json")) { + String base = fileName.substring(0, fileName.length() - 5).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + models.add(base); + } + } else if (lower.endsWith(".png")) { + String base = fileName.substring(0, fileName.length() - 4).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + textures.add(base); + } + } + } + + for (String model : models) { + if (textures.contains(model)) { + out.add(model); + } + } + } catch (IOException ignored) { + } + } + + private static boolean isArchive(Path path) { + String name = path.getFileName().toString().toLowerCase(Locale.ROOT); + return name.endsWith(".zip") || name.endsWith(".jar"); + } +} diff --git a/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json b/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json index 9b34656..00c4e98 100644 --- a/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json +++ b/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json @@ -411,6 +411,15 @@ "command.box3.box3barrier.status": "Barrier visible: %s", "command.box3.box3barrier.set": "Barrier visibility set to: %s", "command.box3.box3barrier.toggled": "Barrier visibility toggled to: %s (re-enter the world to fully apply)", - "item.box3.model_destroyer": "Model destruction bucket", + "message.box3.model.config.mode": "Model config mode: %s (Stick: +, Blaze Rod: -, Paper: copy, Book: paste)", + "message.box3.model.config.status": "mode=%s scale=%s offset=(%s, %s, %s) rot=%s", + "message.box3.model.config.mode.scale": "Scale", + "message.box3.model.config.mode.offset_x": "Offset X", + "message.box3.model.config.mode.offset_y": "Offset Y", + "message.box3.model.config.mode.offset_z": "Offset Z", + "message.box3.model.config.mode.rotation": "Rotation", + "message.box3.model.config.copy.success": "Copied current model config.", + "message.box3.model.config.copy.empty": "No copied config found.", + "message.box3.model.config.copy.pasted": "Pasted copied model config.", "flat_world_preset.box3.box3_plains_world": "Box3 Plains" } diff --git a/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json b/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json index 29bebcc..82b8d1e 100644 --- a/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json +++ b/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json @@ -399,6 +399,15 @@ "command.box3.box3barrier.status": "屏障可见状态:%s", "command.box3.box3barrier.set": "屏障可见状态已设置为:%s", "command.box3.box3barrier.toggled": "屏障可见状态已切换为:%s(重新进入世界以完全生效)", - "item.box3.model_destroyer": "模型销毁桶", + "message.box3.model.config.mode": "模型配置模式:%s(木棍:增加,烈焰棒:减少,纸:复制,书:粘贴)", + "message.box3.model.config.status": "模式=%s 缩放=%s 偏移=(%s, %s, %s) 旋转=%s", + "message.box3.model.config.mode.scale": "缩放", + "message.box3.model.config.mode.offset_x": "X偏移", + "message.box3.model.config.mode.offset_y": "Y偏移", + "message.box3.model.config.mode.offset_z": "Z偏移", + "message.box3.model.config.mode.rotation": "旋转", + "message.box3.model.config.copy.success": "已复制当前模型参数。", + "message.box3.model.config.copy.empty": "没有可粘贴的已复制参数。", + "message.box3.model.config.copy.pasted": "已粘贴模型参数。", "flat_world_preset.box3.box3_plains_world": "神岛平原" } diff --git a/README.md b/README.md index f7a81fc..b691001 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,18 @@ - **资源文件导入**:支持从 `resourcepacks/` 目录文件导入资源包。 - **资源包加载模型**:将模型放入资源包即可自动注册到创造模式。 - **模型物品标签页**:`Box3:模型` 标签页用于管理模型物品。 -- **模型销毁器**:右键模型可删除(道具名:模型销毁器)。 - **生成模型资源包**:访问 https://box3lab.com/mc-resource-pack 获取适用于本模组的资源包文件。 +#### ✨ 操作模型说明 + +- **交互调参**: + - `空手`右键模型:切换模式(缩放 / X偏移 / Y偏移 / Z偏移 / 旋转) + - `木棍`右键模型:当前模式参数增加 + - `烈焰棒`右键模型:当前模式参数减少 +- **参数复制粘贴**: + - `纸`右键模型:复制当前模型参数 + - `书`右键模型:粘贴参数到目标模型模型 + ### 🔍 屏障可见性切换 - **屏障可见性切换 `/box3barrier`**: diff --git a/README_en.md b/README_en.md index ced4748..df3301e 100644 --- a/README_en.md +++ b/README_en.md @@ -50,6 +50,16 @@ You can also migrate structures from Box3 directly into your Minecraft world, pr - **Model Destroyer**: Right-click a model to delete it (item name: Model Destroyer). - **Generate model resource pack**: Visit https://box3lab.com/mc-resource-pack to get a pack compatible with this mod. +#### ✨ Model Operation Guide + +- **Interactive parameter tuning**: + - Right-click the model with an `empty hand`: switch mode (`Scale / Offset X / Offset Y / Offset Z / Rotation`) + - Right-click the model with a `Stick`: increase the current mode value + - Right-click the model with a `Blaze Rod`: decrease the current mode value +- **Copy & paste parameters**: + - Right-click the model with `Paper`: copy current model parameters + - Right-click the model with a `Book`: paste parameters to the target model + ### 🔍 Barrier Visibility Toggle - **Barrier visibility command `/box3barrier`**: