getKnownBlocks() {
- return ModBlocks.BLOCKS.getEntries().stream().map(h -> (Block) h.value()).toList();
- }
-
- @Override
- protected void generate() {
- // 焚烬木套件
- dropSelf(ModBlocks.BLAZE_LOG.get());
- dropSelf(ModBlocks.BLAZE_WOOD.get());
- dropSelf(ModBlocks.STRIPPED_BLAZE_LOG.get());
- dropSelf(ModBlocks.STRIPPED_BLAZE_WOOD.get());
- dropSelf(ModBlocks.BLAZE_PLANKS.get());
- dropSelf(ModBlocks.BLAZE_STAIRS.get());
- dropSelf(ModBlocks.BLAZE_SLAB.get());
- dropSelf(ModBlocks.BLAZE_FENCE.get());
- dropSelf(ModBlocks.BLAZE_FENCE_GATE.get());
- dropSelf(ModBlocks.BLAZE_PRESSURE_PLATE.get());
- dropSelf(ModBlocks.BLAZE_BUTTON.get());
- // 树叶 - 使用树叶掉落逻辑
- add(ModBlocks.BLAZE_LEAVES.get(), createLeavesDrops(ModBlocks.BLAZE_LEAVES.get(), ModBlocks.BLAZE_SAPLING.get(), 0.05F));
- dropSelf(ModBlocks.BLAZE_SAPLING.get());
- // 门需要特殊处理
- add(ModBlocks.BLAZE_DOOR.get(), createDoorTable(ModBlocks.BLAZE_DOOR.get()));
- add(ModBlocks.BLAZE_TRAPDOOR.get(), createDoorTable(ModBlocks.BLAZE_TRAPDOOR.get()));
-
- // 灵焰木套件
- dropSelf(ModBlocks.SOUL_LOG.get());
- dropSelf(ModBlocks.SOUL_WOOD.get());
- dropSelf(ModBlocks.STRIPPED_SOUL_LOG.get());
- dropSelf(ModBlocks.STRIPPED_SOUL_WOOD.get());
- dropSelf(ModBlocks.SOUL_PLANKS.get());
- dropSelf(ModBlocks.SOUL_STAIRS.get());
- dropSelf(ModBlocks.SOUL_SLAB.get());
- dropSelf(ModBlocks.SOUL_FENCE.get());
- dropSelf(ModBlocks.SOUL_FENCE_GATE.get());
- dropSelf(ModBlocks.SOUL_PRESSURE_PLATE.get());
- dropSelf(ModBlocks.SOUL_BUTTON.get());
- // 树叶 - 使用树叶掉落逻辑
- add(ModBlocks.SOUL_LEAVES.get(), createLeavesDrops(ModBlocks.SOUL_LEAVES.get(), ModBlocks.SOUL_SAPLING.get(), 0.05F));
- dropSelf(ModBlocks.SOUL_SAPLING.get());
- // 门需要特殊处理
- add(ModBlocks.SOUL_DOOR.get(), createDoorTable(ModBlocks.SOUL_DOOR.get()));
- add(ModBlocks.SOUL_TRAPDOOR.get(), createDoorTable(ModBlocks.SOUL_TRAPDOOR.get()));
-
- // 杜鹃木套件
- dropSelf(ModBlocks.AZALEA_LOG.get());
- dropSelf(ModBlocks.AZALEA_WOOD.get());
- dropSelf(ModBlocks.STRIPPED_AZALEA_LOG.get());
- dropSelf(ModBlocks.STRIPPED_AZALEA_WOOD.get());
- dropSelf(ModBlocks.AZALEA_PLANKS.get());
- dropSelf(ModBlocks.AZALEA_STAIRS.get());
- dropSelf(ModBlocks.AZALEA_SLAB.get());
- dropSelf(ModBlocks.AZALEA_FENCE.get());
- dropSelf(ModBlocks.AZALEA_FENCE_GATE.get());
- dropSelf(ModBlocks.AZALEA_PRESSURE_PLATE.get());
- dropSelf(ModBlocks.AZALEA_BUTTON.get());
- // 门需要特殊处理
- add(ModBlocks.AZALEA_DOOR.get(), createDoorTable(ModBlocks.AZALEA_DOOR.get()));
- add(ModBlocks.AZALEA_TRAPDOOR.get(), createDoorTable(ModBlocks.AZALEA_TRAPDOOR.get()));
-
- // 其他方块
- dropSelf(ModBlocks.SCORCHED_GRASS_BLOCK.get());
- dropSelf(ModBlocks.SCORCHED_DIRT.get());
- dropSelf(ModBlocks.SCORCHED_GRASS.get());
- dropSelf(ModBlocks.SCORCHED_TWIG.get());
- dropSelf(ModBlocks.TINY_CACTUS.get());
- dropOther(ModBlocks.FLETCHING_TABLE.get(), Items.FLETCHING_TABLE);
- }
-}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/datagen/provider/tag/ModItemTagsProvider.java b/src/hfas/java/org/polaris2023/wildwind/hfas/datagen/provider/tag/ModItemTagsProvider.java
deleted file mode 100644
index a2c4953..0000000
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/datagen/provider/tag/ModItemTagsProvider.java
+++ /dev/null
@@ -1,199 +0,0 @@
-package org.polaris2023.wildwind.hfas.datagen.provider.tag;
-
-import net.minecraft.core.Holder;
-import net.minecraft.core.HolderLookup;
-import net.minecraft.core.registries.Registries;
-import net.minecraft.data.PackOutput;
-import net.minecraft.data.tags.TagAppender;
-import net.minecraft.data.tags.TagsProvider;
-import net.minecraft.tags.BlockItemTagId;
-import net.minecraft.tags.BlockItemTags;
-import net.minecraft.tags.ItemTags;
-import net.minecraft.tags.TagKey;
-import net.minecraft.world.item.Item;
-import net.minecraft.world.item.Items;
-import net.minecraft.world.level.block.Block;
-import net.neoforged.neoforge.common.Tags;
-import org.polaris2023.wildwind.hfas.HFASMod;
-import org.polaris2023.wildwind.hfas.block.ModBlocks;
-import org.polaris2023.wildwind.hfas.tag.ModItemTags;
-
-import java.util.Arrays;
-import java.util.concurrent.CompletableFuture;
-
-/**
- * 物品标签生成器
- * 为木质物品添加原版和模组标签
- *
- * 迁移至26.2时旧有的IntrinsicHolderTagsProvider已被移除。改为父类TagsProvider并由addToTag方法桥接。
- * TODO_FINISHED: 部分item tag key存在缺失问题,已模仿原版行为进行处理,但无法确定这种缺失是否系系统性删除,无法确保对应功能一定可以正常工作。请求测试阶段重点检查。
- * 见[Need Check]注释部分。
- * ——landis, 2026/9/1
- *
- * @author baka4n
- * @since 2026/04/15
- */
-public class ModItemTagsProvider extends TagsProvider- {
-
- public ModItemTagsProvider(PackOutput output, CompletableFuture lookupProvider) {
-// super(output, Registries.ITEM, lookupProvider, item -> item.builtInRegistryHolder().key(), HFASMod.MOD_ID);
- super(output, Registries.ITEM, lookupProvider, HFASMod.MOD_ID);
- }
-
- @Override
- protected void addTags(HolderLookup.Provider provider) {
- // ==================== 焚烬木物品标签 ====================
- registerWoodItemTags(
- ModBlocks.BLAZE_LOG.get(),
- ModBlocks.BLAZE_WOOD.get(),
- ModBlocks.STRIPPED_BLAZE_LOG.get(),
- ModBlocks.STRIPPED_BLAZE_WOOD.get(),
- ModBlocks.BLAZE_PLANKS.get(),
- ModBlocks.BLAZE_STAIRS.get(),
- ModBlocks.BLAZE_SLAB.get(),
- ModBlocks.BLAZE_FENCE.get(),
- ModBlocks.BLAZE_FENCE_GATE.get(),
- ModBlocks.BLAZE_DOOR.get(),
- ModBlocks.BLAZE_TRAPDOOR.get(),
- ModBlocks.BLAZE_PRESSURE_PLATE.get(),
- ModBlocks.BLAZE_BUTTON.get(),
- ModBlocks.BLAZE_LEAVES.get(),
- ModBlocks.BLAZE_SAPLING.get()
- );
-
- // ==================== 灵焰木物品标签 ====================
- registerWoodItemTags(
- ModBlocks.SOUL_LOG.get(),
- ModBlocks.SOUL_WOOD.get(),
- ModBlocks.STRIPPED_SOUL_LOG.get(),
- ModBlocks.STRIPPED_SOUL_WOOD.get(),
- ModBlocks.SOUL_PLANKS.get(),
- ModBlocks.SOUL_STAIRS.get(),
- ModBlocks.SOUL_SLAB.get(),
- ModBlocks.SOUL_FENCE.get(),
- ModBlocks.SOUL_FENCE_GATE.get(),
- ModBlocks.SOUL_DOOR.get(),
- ModBlocks.SOUL_TRAPDOOR.get(),
- ModBlocks.SOUL_PRESSURE_PLATE.get(),
- ModBlocks.SOUL_BUTTON.get(),
- ModBlocks.SOUL_LEAVES.get(),
- ModBlocks.SOUL_SAPLING.get()
- );
-
- // ==================== 杜鹃木物品标签 ====================
- registerWoodItemTags(
- ModBlocks.AZALEA_LOG.get(),
- ModBlocks.AZALEA_WOOD.get(),
- ModBlocks.STRIPPED_AZALEA_LOG.get(),
- ModBlocks.STRIPPED_AZALEA_WOOD.get(),
- ModBlocks.AZALEA_PLANKS.get(),
- ModBlocks.AZALEA_STAIRS.get(),
- ModBlocks.AZALEA_SLAB.get(),
- ModBlocks.AZALEA_FENCE.get(),
- ModBlocks.AZALEA_FENCE_GATE.get(),
- ModBlocks.AZALEA_DOOR.get(),
- ModBlocks.AZALEA_TRAPDOOR.get(),
- ModBlocks.AZALEA_PRESSURE_PLATE.get(),
- ModBlocks.AZALEA_BUTTON.get(),
- null, // 杜鹃木使用原版树叶
- null // 杜鹃木使用原版树苗
- );
-
- tag(Tags.Items.ANIMAL_FOODS)
- .addTag(ModItemTags.MUDCRAB_FOOD);
- addToTag(ModItemTags.MUDCRAB_FOOD, Items.SPIDER_EYE);
- }
-
- /**
- * 注册木质物品标签
- */
- private void registerWoodItemTags(
- Block log, Block wood,
- Block strippedLog, Block strippedWood,
- Block planks, Block stairs, Block slab,
- Block fence, Block fenceGate,
- Block door, Block trapdoor,
- Block pressurePlate, Block button,
- Block leaves, Block sapling) {
-
- // 原木物品标签
- addToTag(ItemTags.LOGS, log, wood, strippedLog, strippedWood);
-// tag(ItemTags.LOGS)
-// .add(log.asItem())
-// .add(wood.asItem())
-// .add(strippedLog.asItem())
-// .add(strippedWood.asItem());
-
- // 木板物品标签
- addToTag(ItemTags.PLANKS, planks);
-
- // 木质楼梯物品标签
- addToTag(ItemTags.WOODEN_STAIRS,stairs);
-
- // 木质台阶物品标签
- addToTag(ItemTags.WOODEN_SLABS, slab);
-
- // 木质栅栏物品标签
- addToTag(Tags.Items.FENCES_WOODEN, fence);
-
- // 栅栏物品标签
- // 栅栏tag默认包含"#c:fences/wooden",弃用
-// tag(Tags.Items.FENCES)
-// .add(fence.asItem());
-
- // 木质栅栏门物品标签
- addToTag(Tags.Items.FENCE_GATES_WOODEN, fenceGate);
-
- // 栅栏门物品标签
- addToTag(ItemTags.FENCE_GATES, fenceGate);
-
- // 木质门物品标签
- addToTag(ItemTags.WOODEN_DOORS,door);
-
- // 门物品标签
- // bugjump不知道给这玩意更新到哪去了 item没key 难绷
- // [NeedCheck]
- addToTag(BlockItemTags.DOORS.item(), door);
-
- // 活板门物品标签
- // 续上:我要点名某些明明有包含关系但是就是不引用子tag还要把key删掉的tag
- // [NeedCheck]
- addToTag(BlockItemTags.TRAPDOORS.item(), trapdoor);
-
- // 木质活板门物品标签
- addToTag(ItemTags.WOODEN_TRAPDOORS, trapdoor);
-
- // 木质压力板物品标签
- addToTag(ItemTags.WOODEN_PRESSURE_PLATES, pressurePlate);
-
- // 木质按钮物品标签
- addToTag(ItemTags.WOODEN_BUTTONS, button);
-
- // 按钮物品标签
- // 不是哥们 你怎么也没了我朝为
- // [NeedCheck]
- addToTag(BlockItemTags.BUTTONS.item(), button);
-
- // 树叶物品标签
- if (leaves != null) {
- addToTag(ItemTags.LEAVES, leaves);
- }
-
- // 树苗物品标签
- if (sapling != null) {
- addToTag(ItemTags.SAPLINGS, sapling);
- }
- }
-
- @SuppressWarnings("all")
- public void addToTag(TagKey
- tag, Block... items){
- if(items.length == 0) return;
- tag(tag).addAll(Arrays.stream(items).map(Block::asItem).map(Item::builtInRegistryHolder).map(Holder.Reference::getKey));
- }
-
- @SuppressWarnings("all")
- public void addToTag(TagKey
- tag, Item... items){
- if(items.length == 0) return;
- tag(tag).addAll(Arrays.stream(items).map(Item::builtInRegistryHolder).map(Holder.Reference::getKey));
- }
-}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/event/EntitiesHandler.java b/src/hfas/java/org/polaris2023/wildwind/hfas/event/EntitiesHandler.java
deleted file mode 100644
index 7d1db27..0000000
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/event/EntitiesHandler.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.polaris2023.wildwind.hfas.event;
-
-import net.minecraft.world.level.levelgen.Heightmap;
-import net.neoforged.bus.api.SubscribeEvent;
-import net.neoforged.fml.common.EventBusSubscriber;
-import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent;
-import net.neoforged.neoforge.event.entity.RegisterSpawnPlacementsEvent;
-import org.polaris2023.wildwind.hfas.entity.animal.Mudcrab;
-import org.polaris2023.wildwind.hfas.registry.ModEntities;
-
-@EventBusSubscriber
-public class EntitiesHandler {
- @SubscribeEvent
- static void onAttributeCreate(EntityAttributeCreationEvent event) {
- event.put(ModEntities.MUDCRAB.get(), Mudcrab.createAttributes().build());
- }
-
- @SubscribeEvent
- static void registerSpawnPlacements(RegisterSpawnPlacementsEvent event) {
- event.register(ModEntities.MUDCRAB.get(), Mudcrab.SPAWN_PLACEMENT, Heightmap.Types.OCEAN_FLOOR, Mudcrab::checkMudcrabInWaterGroundSpawnRules, RegisterSpawnPlacementsEvent.Operation.REPLACE);
- event.register(ModEntities.MUDCRAB.get(), Mudcrab::checkMudcrabOnGroundSpawnRules);
- }
-}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/event/FletchingTableReplaceHandler.java b/src/hfas/java/org/polaris2023/wildwind/hfas/event/FletchingTableReplaceHandler.java
deleted file mode 100644
index b397c17..0000000
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/event/FletchingTableReplaceHandler.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package org.polaris2023.wildwind.hfas.event;
-
-import net.minecraft.core.BlockPos;
-import net.minecraft.world.level.Level;
-import net.minecraft.world.level.LevelAccessor;
-import net.minecraft.world.level.block.Blocks;
-import net.minecraft.world.level.block.state.BlockState;
-import net.minecraft.world.level.block.state.properties.Property;
-import net.minecraft.world.level.chunk.LevelChunk;
-import net.minecraft.world.phys.AABB;
-import net.neoforged.bus.api.EventPriority;
-import net.neoforged.bus.api.SubscribeEvent;
-import net.neoforged.fml.common.EventBusSubscriber;
-import net.neoforged.neoforge.event.level.BlockEvent;
-import net.neoforged.neoforge.event.level.ChunkDataEvent;
-import net.neoforged.neoforge.event.level.ChunkEvent;
-import org.polaris2023.wildwind.hfas.HFASMod;
-import org.polaris2023.wildwind.hfas.block.ModBlocks;
-import org.polaris2023.wildwind.hfas.config.ModCommonConfig;
-
-/**
- * 世界生成事件处理器
- * 用于替换结构内的原版制箭台为模组制箭台
- *
- * @author baka4n
- * @since 2026/04/15
- */
-@EventBusSubscriber(modid = HFASMod.MOD_ID)
-public class FletchingTableReplaceHandler {
-
- /**
- * 方块放置事件 - 处理所有制箭台放置
- * 包括结构生成、玩家放置等
- */
- @SubscribeEvent(priority = EventPriority.HIGHEST)
- public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) {
- if (!ModCommonConfig.shouldReplaceVanillaFletchingTable()) {
- return;
- }
-
- BlockState placedState = event.getPlacedBlock();
-
- if (placedState.getBlock() == Blocks.FLETCHING_TABLE) {
- LevelAccessor levelAccessor = event.getLevel();
-
- if (levelAccessor instanceof Level level && !level.isClientSide()) {
- BlockPos pos = event.getPos();
- // 取消原事件并设置模组制箭台
- event.setCanceled(true);
- level.setBlock(pos, ModBlocks.FLETCHING_TABLE.get().defaultBlockState(), 3);
- }
- }
- }
-
- /**
- * 区块加载事件 - 替换区块内的原版制箭台
- */
- @SuppressWarnings({"unchecked", "rawtypes"})
- @SubscribeEvent(priority = EventPriority.HIGHEST)
- public static void onChunkLoad(ChunkEvent.Load event) {
- if (!ModCommonConfig.shouldReplaceVanillaFletchingTable()) {
- return;
- }
-
- LevelChunk chunk = event.getChunk();
- LevelAccessor level = event.getLevel();
-
- if (!level.isClientSide()) {
- BlockPos.betweenClosedStream(new AABB(
- chunk.getPos().getMinBlockX(), level.getMinY(), chunk.getPos().getMinBlockZ(),
- chunk.getPos().getMaxBlockX(), 200, chunk.getPos().getMaxBlockZ()
- )).forEach(pos -> {
- BlockState state = chunk.getBlockState(pos);
- if (state.is(Blocks.FLETCHING_TABLE)) {
- BlockState returnState = ModBlocks.FLETCHING_TABLE.get().defaultBlockState();
- state.getValues().forEach(v -> {
- Property property = v.property();
- Comparable value = v.value();
- returnState.setValue(property, value);
- });
- chunk.setBlockState(pos, returnState);
- }
- });
-
- }
- }
-}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/event/FuelHandler.java b/src/hfas/java/org/polaris2023/wildwind/hfas/event/FuelHandler.java
deleted file mode 100644
index d72eacb..0000000
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/event/FuelHandler.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package org.polaris2023.wildwind.hfas.event;
-
-import net.minecraft.world.item.Item;
-import net.minecraft.world.level.block.Block;
-import net.neoforged.bus.api.SubscribeEvent;
-import net.neoforged.fml.common.EventBusSubscriber;
-import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent;
-import net.neoforged.neoforge.event.furnace.FurnaceFuelBurnTimeEvent;
-import org.polaris2023.wildwind.hfas.HFASMod;
-import org.polaris2023.wildwind.hfas.block.ModBlocks;
-
-/**
- * 燃料事件处理器
- * 为木质方块添加燃料功能
- *
- * @author baka4n
- * @since 2026/04/15
- */
-@EventBusSubscriber(modid = HFASMod.MOD_ID)
-public class FuelHandler {
-
- /**
- * 燃料燃烧时间事件
- * 木质方块可作为燃料,燃烧300 tick(与原版木板相同)
- */
- @SubscribeEvent
- public static void onFuelBurnTime(FurnaceFuelBurnTimeEvent event) {
- Item item = event.getItemStack().getItem();
-
- // 检查是否是木质方块物品
- int burnTime = getWoodBurnTime(item);
- if (burnTime > 0) {
- event.setBurnTime(burnTime);
- }
- }
-
- /**
- * 获取木质方块的燃烧时间
- */
- private static int getWoodBurnTime(Item item) {
- // 焚烬木 - 300 tick(15秒)
- if (isBlazeWoodItem(item)) return 300;
- // 灵焰木 - 300 tick
- if (isSoulWoodItem(item)) return 300;
- // 杜鹃木 - 300 tick
- if (isAzaleaWoodItem(item)) return 300;
-
- return 0;
- }
-
- private static boolean isBlazeWoodItem(Item item) {
- return item == ModBlocks.BLAZE_LOG.get().asItem()
- || item == ModBlocks.BLAZE_WOOD.get().asItem()
- || item == ModBlocks.STRIPPED_BLAZE_LOG.get().asItem()
- || item == ModBlocks.STRIPPED_BLAZE_WOOD.get().asItem()
- || item == ModBlocks.BLAZE_PLANKS.get().asItem()
- || item == ModBlocks.BLAZE_SLAB.get().asItem()
- || item == ModBlocks.BLAZE_FENCE.get().asItem()
- || item == ModBlocks.BLAZE_FENCE_GATE.get().asItem();
- }
-
- private static boolean isSoulWoodItem(Item item) {
- return item == ModBlocks.SOUL_LOG.get().asItem()
- || item == ModBlocks.SOUL_WOOD.get().asItem()
- || item == ModBlocks.STRIPPED_SOUL_LOG.get().asItem()
- || item == ModBlocks.STRIPPED_SOUL_WOOD.get().asItem()
- || item == ModBlocks.SOUL_PLANKS.get().asItem()
- || item == ModBlocks.SOUL_SLAB.get().asItem()
- || item == ModBlocks.SOUL_FENCE.get().asItem()
- || item == ModBlocks.SOUL_FENCE_GATE.get().asItem();
- }
-
- private static boolean isAzaleaWoodItem(Item item) {
- return item == ModBlocks.AZALEA_LOG.get().asItem()
- || item == ModBlocks.AZALEA_WOOD.get().asItem()
- || item == ModBlocks.STRIPPED_AZALEA_LOG.get().asItem()
- || item == ModBlocks.STRIPPED_AZALEA_WOOD.get().asItem()
- || item == ModBlocks.AZALEA_PLANKS.get().asItem()
- || item == ModBlocks.AZALEA_SLAB.get().asItem()
- || item == ModBlocks.AZALEA_FENCE.get().asItem()
- || item == ModBlocks.AZALEA_FENCE_GATE.get().asItem();
- }
-}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/groups/ModCreativeTabs.java b/src/hfas/java/org/polaris2023/wildwind/hfas/groups/ModCreativeTabs.java
deleted file mode 100644
index ac957bb..0000000
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/groups/ModCreativeTabs.java
+++ /dev/null
@@ -1,136 +0,0 @@
-package org.polaris2023.wildwind.hfas.groups;
-
-import net.minecraft.world.item.CreativeModeTab;
-import net.minecraft.world.item.CreativeModeTabs;
-import net.minecraft.world.item.ItemStack;
-import net.minecraft.world.item.Items;
-import net.minecraft.world.level.block.Blocks;
-import net.neoforged.bus.api.SubscribeEvent;
-import net.neoforged.fml.common.EventBusSubscriber;
-import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent;
-import org.polaris2023.wildwind.hfas.HFASMod;
-import org.polaris2023.wildwind.hfas.block.ModBlocks;
-
-/**
- * 创造模式物品栏配置
- * 将模组物品放入原版创造模式物品栏并排序
- *
- * @author baka4n
- * @since 2026/04/15
- */
-//TODO 考虑统一迁移至registry下,同时考虑合入自定义tab
-@EventBusSubscriber(modid = HFASMod.MOD_ID)
-public class ModCreativeTabs {
-
- @SubscribeEvent
- public static void buildCreativeTabContents(BuildCreativeModeTabContentsEvent event) {
- // 建筑方块物品栏
-
- if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) {
- // 杜鹃系列
- {
- ItemStack logStack = ModBlocks.AZALEA_LOG.toStack();
- event.insertAfter(new ItemStack(Blocks.CHERRY_BUTTON), logStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack woodStack = ModBlocks.AZALEA_WOOD.toStack();
- event.insertAfter(logStack, woodStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack strippedLogStack = ModBlocks.STRIPPED_AZALEA_LOG.toStack();
- event.insertAfter(woodStack, strippedLogStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack strippedWoodStack = ModBlocks.STRIPPED_AZALEA_WOOD.toStack();
- event.insertAfter(strippedLogStack, strippedWoodStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack planksStack = ModBlocks.AZALEA_PLANKS.toStack();
- event.insertAfter(strippedWoodStack, planksStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack stairsStack = ModBlocks.AZALEA_STAIRS.toStack();
- event.insertAfter(planksStack, stairsStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack slabStack = ModBlocks.AZALEA_SLAB.toStack();
- event.insertAfter(stairsStack, slabStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack fenceStack = ModBlocks.AZALEA_FENCE.toStack();
- event.insertAfter(slabStack, fenceStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack fenceGateStack = ModBlocks.AZALEA_FENCE_GATE.toStack();
- event.insertAfter(fenceStack, fenceGateStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack doorStack = ModBlocks.AZALEA_DOOR.toStack();
- event.insertAfter(fenceGateStack, doorStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack trapdoorStack = ModBlocks.AZALEA_TRAPDOOR.toStack();
- event.insertAfter(doorStack, trapdoorStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- event.insertAfter(trapdoorStack, ModBlocks.AZALEA_PRESSURE_PLATE.toStack(), CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- }
- // 焚烬系列
- {
- ItemStack logStack = ModBlocks.BLAZE_LOG.toStack();
- event.insertAfter(new ItemStack(Blocks.BAMBOO_BUTTON), logStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack woodStack = ModBlocks.BLAZE_WOOD.toStack();
- event.insertAfter(logStack, woodStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack strippedLogStack = ModBlocks.STRIPPED_BLAZE_LOG.toStack();
- event.insertAfter(woodStack, strippedLogStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack strippedWoodStack = ModBlocks.STRIPPED_BLAZE_WOOD.toStack();
- event.insertAfter(strippedLogStack, strippedWoodStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack planksStack = ModBlocks.BLAZE_PLANKS.toStack();
- event.insertAfter(strippedWoodStack, planksStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack stairsStack = ModBlocks.BLAZE_STAIRS.toStack();
- event.insertAfter(planksStack, stairsStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack slabStack = ModBlocks.BLAZE_SLAB.toStack();
- event.insertAfter(stairsStack, slabStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack fenceStack = ModBlocks.BLAZE_FENCE.toStack();
- event.insertAfter(slabStack, fenceStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack fenceGateStack = ModBlocks.BLAZE_FENCE_GATE.toStack();
- event.insertAfter(fenceStack, fenceGateStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack doorStack = ModBlocks.BLAZE_DOOR.toStack();
- event.insertAfter(fenceGateStack, doorStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack trapdoorStack = ModBlocks.BLAZE_TRAPDOOR.toStack();
- event.insertAfter(doorStack, trapdoorStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- event.insertAfter(trapdoorStack, ModBlocks.BLAZE_PRESSURE_PLATE.toStack(), CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- }
- // 灵焰系列
- {
- ItemStack logStack = ModBlocks.SOUL_LOG.toStack();
- event.insertAfter(ModBlocks.BLAZE_PRESSURE_PLATE.toStack(), logStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack woodStack = ModBlocks.SOUL_WOOD.toStack();
- event.insertAfter(logStack, woodStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack strippedLogStack = ModBlocks.STRIPPED_SOUL_LOG.toStack();
- event.insertAfter(woodStack, strippedLogStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack strippedWoodStack = ModBlocks.STRIPPED_SOUL_WOOD.toStack();
- event.insertAfter(strippedLogStack, strippedWoodStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack planksStack = ModBlocks.SOUL_PLANKS.toStack();
- event.insertAfter(strippedWoodStack, planksStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack stairsStack = ModBlocks.SOUL_STAIRS.toStack();
- event.insertAfter(planksStack, stairsStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack slabStack = ModBlocks.SOUL_SLAB.toStack();
- event.insertAfter(stairsStack, slabStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack fenceStack = ModBlocks.SOUL_FENCE.toStack();
- event.insertAfter(slabStack, fenceStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack fenceGateStack = ModBlocks.SOUL_FENCE_GATE.toStack();
- event.insertAfter(fenceStack, fenceGateStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack doorStack = ModBlocks.SOUL_DOOR.toStack();
- event.insertAfter(fenceGateStack, doorStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack trapdoorStack = ModBlocks.SOUL_TRAPDOOR.toStack();
- event.insertAfter(doorStack, trapdoorStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- event.insertAfter(trapdoorStack, ModBlocks.SOUL_PRESSURE_PLATE.toStack(), CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- }
-
-
- // 焦灰草方块
-// event.accept(ModBlocks.SCORCHED_GRASS_BLOCK.get());
-// // 焦土
-// event.accept(ModBlocks.SCORCHED_DIRT.get());
- }
-
- // 自然方块物品栏
- if (event.getTabKey() == CreativeModeTabs.NATURAL_BLOCKS) {
- ItemStack azaleaLogStack = ModBlocks.AZALEA_LOG.toStack();
- event.insertAfter(new ItemStack(Items.CHERRY_LOG), azaleaLogStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- ItemStack blazeLogStack = ModBlocks.BLAZE_LOG.toStack();
- event.insertAfter(azaleaLogStack, blazeLogStack, CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- event.insertAfter(blazeLogStack, ModBlocks.SOUL_LOG.toStack(), CreativeModeTab.TabVisibility.PARENT_AND_SEARCH_TABS);
- // 焦灰草丛
-// event.accept(ModBlocks.SCORCHED_GRASS.get());
-// // 焦灰枝条
-// event.accept(ModBlocks.SCORCHED_TWIG.get());
-// // 仙人球
-// event.accept(ModBlocks.TINY_CACTUS.get());
-// // 焦灰草方块
-// event.accept(ModBlocks.SCORCHED_GRASS_BLOCK.get());
-// // 焦土
-// event.accept(ModBlocks.SCORCHED_DIRT.get());
- }
-
- }
-}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/menu/FletchingTableMenu.java b/src/hfas/java/org/polaris2023/wildwind/hfas/menu/FletchingTableMenu.java
deleted file mode 100644
index 29f4e81..0000000
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/menu/FletchingTableMenu.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package org.polaris2023.wildwind.hfas.menu;
-
-import net.minecraft.world.Container;
-import net.minecraft.world.SimpleContainer;
-import net.minecraft.world.entity.player.Inventory;
-import net.minecraft.world.entity.player.Player;
-import net.minecraft.world.inventory.AbstractContainerMenu;
-import net.minecraft.world.inventory.ContainerLevelAccess;
-import net.minecraft.world.inventory.Slot;
-import net.minecraft.world.item.ItemStack;
-import org.polaris2023.wildwind.hfas.block.ModBlocks;
-import org.polaris2023.wildwind.hfas.block.entity.FletchingTableBlockEntity;
-
-import javax.annotation.Nullable;
-import java.util.Optional;
-
-/**
- * 制箭台菜单
- *
- * @author baka4n
- * @since 2026/04/15
- */
-public class FletchingTableMenu extends AbstractContainerMenu {
- private final Container container;
- private final ContainerLevelAccess access;
-
- public FletchingTableMenu(int containerId, Inventory playerInventory) {
- this(containerId, playerInventory, new SimpleContainer(4), ContainerLevelAccess.NULL);
- }
-
- public FletchingTableMenu(int containerId, Inventory playerInventory, Container container, ContainerLevelAccess access) {
- super(ModMenus.FLETCHING_TABLE.get(), containerId);
- this.container = container;
- this.access = access;
-
- // 箭羽槽 (slot 0)
- this.addSlot(FletchingTableBlockEntity.Slots.SLOT_FLETCHING.toSlot(container, 26, 18));
- // 箭杆槽 (slot 1)
- this.addSlot(FletchingTableBlockEntity.Slots.SLOT_SHAFT.toSlot(container, 62, 18));
- // 箭头槽 (slot 2)
- this.addSlot( FletchingTableBlockEntity.Slots.SLOT_HEAD.toSlot(container, 98, 18));
- // 燃料槽口(slot 3)
- this.addSlot(FletchingTableBlockEntity.Slots.SLOT_FUEL.toSlot(container, 62, 68));
- // 输出槽 (slot 4)
- this.addSlot(this.addSlot(FletchingTableBlockEntity.Slots.SLOT_RESULT.toSlot(container, 134, 18)));
-
- // 添加玩家物品栏
- for (int row = 0; row < 3; ++row) {
- for (int col = 0; col < 9; ++col) {
- this.addSlot(new Slot(playerInventory, col + row * 9 + 9, 8 + col * 18, 50 + row * 18));
- }
- }
-
- // 添加玩家快捷栏
- for (int col = 0; col < 9; ++col) {
- this.addSlot(new Slot(playerInventory, col, 8 + col * 18, 108));
- }
- }
-
- /**
- * 输出槽 - 不能放入物品,取出时消耗材料
- */
- private class ResultSlot extends Slot {
- public ResultSlot(Container container, int slot, int x, int y) {
- super(container, slot, x, y);
- }
-
- @Override
- public boolean mayPlace(ItemStack stack) {
- return false;
- }
-
- @Override
- public void onTake(Player player, ItemStack stack) {
- // 消耗输入材料
-// container.removeItem(FletchingTableBlockEntity.SLOT_FLETCHING, 1);
-// container.removeItem(FletchingTableBlockEntity.SLOT_SHAFT, 1);
-// container.removeItem(FletchingTableBlockEntity.SLOT_HEAD, 1);
- super.onTake(player, stack);
- }
- }
-
- @Override
- public ItemStack quickMoveStack(Player player, int index) {
- ItemStack itemStack = ItemStack.EMPTY;
- Slot slot = this.slots.get(index);
-
- if (slot != null && slot.hasItem()) {
- ItemStack slotItem = slot.getItem();
- itemStack = slotItem.copy();
-
- // 输出槽 (index 3)
- if (index == 3) {
- if (!this.moveItemStackTo(slotItem, 4, this.slots.size(), true)) {
- return ItemStack.EMPTY;
- }
- slot.onTake(player, slotItem);
- }
- // 输入槽 (index 0-2)
- else if (index >= 0 && index < 3) {
- if (!this.moveItemStackTo(slotItem, 4, this.slots.size(), true)) {
- return ItemStack.EMPTY;
- }
- }
- // 玩家物品栏
- else {
- // 尝试放入输入槽
- if (!this.moveItemStackTo(slotItem, 0, 3, false)) {
- return ItemStack.EMPTY;
- }
- }
-
- if (slotItem.isEmpty()) {
- slot.set(ItemStack.EMPTY);
- } else {
- slot.setChanged();
- }
- }
-
- return itemStack;
- }
-
- @Override
- public boolean stillValid(Player player) {
- return stillValid(this.access, player, ModBlocks.FLETCHING_TABLE.get());
- }
-
-}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/registry/ModCreativeTabs.java b/src/hfas/java/org/polaris2023/wildwind/hfas/registry/ModCreativeTabs.java
deleted file mode 100644
index a89ef9d..0000000
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/registry/ModCreativeTabs.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.polaris2023.wildwind.hfas.registry;
-
-import net.minecraft.core.registries.Registries;
-import net.minecraft.network.chat.Component;
-import net.minecraft.world.item.CreativeModeTab;
-import net.neoforged.bus.api.IEventBus;
-import net.neoforged.neoforge.registries.DeferredHolder;
-import net.neoforged.neoforge.registries.DeferredRegister;
-import org.polaris2023.wildwind.hfas.HFASMod;
-
-public class ModCreativeTabs {
- public static final DeferredRegister CREATIVE_TABS =
- DeferredRegister.create(Registries.CREATIVE_MODE_TAB, HFASMod.MOD_ID);
- /**
- * 模组专用创造模式物品栏喵~
- */
- public static final DeferredHolder WILD_WIND =
- CREATIVE_TABS.register("wild_wind", () -> CreativeModeTab.builder()
- .title(Component.translatable("mod.wild_wind.name"))
-// .icon(() -> ModItems.CINDER.log().get().getDefaultInstance()) TODO 换成对应的物品
- .icon(() -> ModItems.MUDCRAB_BUCKET.get().getDefaultInstance())
- .displayItems((params, output) -> addStacks(output))
- .build()
- );
-
- /**
- * 向模组事件总线注册创造模式物品栏喵~
- *
- * @param modBus 模组事件总线喵~
- */
- public static void register(IEventBus modBus) {
- CREATIVE_TABS.register(modBus);
- }
-
- private static void addStacks(CreativeModeTab.Output output){
- output.accept(ModItems.MUDCRAB_BUCKET);
- output.accept(ModItems.MUDCRAB_SPAWN_EGG);
- output.accept(ModItems.CRAB_CLAW);
- output.accept(ModItems.OMNI_CLAW);
- }
-}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/registry/ModEntities.java b/src/hfas/java/org/polaris2023/wildwind/hfas/registry/ModEntities.java
deleted file mode 100644
index 1f08a6c..0000000
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/registry/ModEntities.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.polaris2023.wildwind.hfas.registry;
-
-import net.minecraft.world.entity.EntityType;
-import net.minecraft.world.entity.MobCategory;
-import net.neoforged.bus.api.IEventBus;
-import net.neoforged.neoforge.registries.DeferredHolder;
-import net.neoforged.neoforge.registries.DeferredRegister;
-import org.polaris2023.wildwind.hfas.HFASMod;
-import org.polaris2023.wildwind.hfas.client.eventsub.EntitiesClientHandler;
-import org.polaris2023.wildwind.hfas.entity.animal.Mudcrab;
-
-
-public class ModEntities {
- public static final DeferredRegister.Entities ENTITY_TYPES =
- DeferredRegister.createEntities(HFASMod.MOD_ID);
-
- private ModEntities() {
- }
-
- //泥沼蟹实体类型
- public static final DeferredHolder, EntityType> MUDCRAB =
- ENTITY_TYPES.registerEntityType("mudcrab", Mudcrab::new, MobCategory.WATER_CREATURE,
- builder -> builder.sized(0.5F, 0.55F).clientTrackingRange(8)
- );
-
- /**
- * 向模组事件总线注册实体类型喵~
- *
- * @see EntitiesClientHandler EntitiesClientEvent 实体客户端渲染绑定
- * @see org.polaris2023.wildwind.hfas.event.EntitiesHandler EntitiesHandler 实体服务端信息(attribute等绑定)
- *
- * @param modBus 模组事件总线喵~
- */
- public static void register(IEventBus modBus) {
- ENTITY_TYPES.register(modBus);
- }
-
-}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/worldgen/ModTreeGrowers.java b/src/hfas/java/org/polaris2023/wildwind/hfas/worldgen/ModTreeGrowers.java
deleted file mode 100644
index 034c406..0000000
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/worldgen/ModTreeGrowers.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.polaris2023.wildwind.hfas.worldgen;
-
-import net.minecraft.resources.ResourceKey;
-import net.minecraft.world.level.block.grower.TreeGrower;
-import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
-import org.polaris2023.wildwind.hfas.HFASMod;
-
-import java.util.Optional;
-
-/**
- * 树木生长器注册
- *
- * @author baka4n
- * @since 2026/04/15
- */
-public class ModTreeGrowers {
-
- // 焚烬木树苗生长器
- public static final TreeGrower BLAZE = new TreeGrower(
- "blaze",
- Optional.empty(),
- Optional.of(ModConfiguredFeatures.BLAZE_TREE_KEY),
- Optional.empty()
- );
-
- // 灵焰木树苗生长器
- public static final TreeGrower SOUL = new TreeGrower(
- "soul",
- Optional.empty(),
- Optional.of(ModConfiguredFeatures.SOUL_TREE_KEY),
- Optional.empty()
- );
-
- // 杜鹃木树苗生长器
- public static final TreeGrower AZALEA = new TreeGrower(
- "azalea",
- Optional.empty(),
- Optional.of(ModConfiguredFeatures.AZALEA_TREE_KEY),
- Optional.empty()
- );
-}
diff --git a/src/hfas/resources/ww_hfas.mixins.json b/src/hfas/resources/ww_hfas.mixins.json
deleted file mode 100644
index 13489e6..0000000
--- a/src/hfas/resources/ww_hfas.mixins.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "required": true,
- "package": "org.polaris2023.wildwind.hfas.mixin",
- "compatibilityLevel": "JAVA_25",
- "mixins": [
- "AzaleaBlockMixin",
- "EnchantmentHelperMixin",
- "ItemStackMixin",
- "MobMixin",
- "PlayerMixin",
- "ServerPlayerGameModeMixin"
- ],
- "injectors": {
- "defaultRequire": 1
- },
- "overwrites": {
- "requireAnnotations": true
- }
-}
diff --git a/src/smoke/java/org/polaris2023/wildwind/smoke/ClientInstallationTests.java b/src/smoke/java/org/polaris2023/wildwind/smoke/ClientInstallationTests.java
new file mode 100644
index 0000000..42ad522
--- /dev/null
+++ b/src/smoke/java/org/polaris2023/wildwind/smoke/ClientInstallationTests.java
@@ -0,0 +1,47 @@
+package org.polaris2023.wildwind.smoke;
+
+import com.mojang.logging.LogUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.TitleScreen;
+import net.minecraft.resources.Identifier;
+import net.neoforged.api.distmarker.Dist;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.fml.ModList;
+import net.neoforged.fml.common.EventBusSubscriber;
+import net.neoforged.neoforge.client.event.ClientTickEvent;
+
+/** Opt-in client resource-load check; never packaged in a release mod. */
+@EventBusSubscriber(modid = "wild_wind_smoke", value = Dist.CLIENT)
+public final class ClientInstallationTests {
+ @SubscribeEvent
+ public static void tick(ClientTickEvent.Post event) {
+ if (!Boolean.getBoolean("wildwind.clientSmoke")) return;
+ var client = Minecraft.getInstance();
+ if (client.gui.overlay() != null || !(client.gui.screen() instanceof TitleScreen)) return;
+ require(client, "wild_wind_core:textures/entity/signs/azalea.png");
+ // Verify conservative server pagination against the actual loaded Minecraft font.
+ var pages = org.polaris2023.wildwind.core.inspection.BookPages.paginate(java.util.List.of(
+ "原野之风·属性记录", "物品ID:wild_wind_wilderness:muddy_pig_spawn_egg",
+ "活塞推动行为:不可推动,不可拉动", "烧炼时间:15秒(300刻)", "空气阻力修正系数:0.01"));
+ for (var page : pages) {
+ if (client.font.wordWrapHeight(net.minecraft.network.chat.Component.literal(page), 114) > 126) throw new IllegalStateException("Inspector page exceeds book height");
+ for (var line : page.split("\n")) {
+ if (client.font.width(line) > 114) throw new IllegalStateException("Inspector line exceeds book width: " + line);
+ }
+ }
+ if (ModList.get().isLoaded("wild_wind_wilderness")) {
+ require(client, "wild_wind_wilderness:textures/entity/pig/pig_muddy.png");
+ require(client, "wild_wind_wilderness:textures/entity/pig/pig_muddy_baby.png");
+ require(client, "wild_wind_wilderness:textures/entity/piranha.png");
+ require(client, "wild_wind_wilderness:geckolib/models/entity/mudcrab.geo.json");
+ }
+ LogUtils.getLogger().info("Wild Wind client installation check passed");
+ client.stop();
+ }
+
+ private static void require(Minecraft client, String id) {
+ if (client.getResourceManager().getResource(Identifier.parse(id)).isEmpty()) {
+ throw new IllegalStateException("Missing module resource: " + id);
+ }
+ }
+}
diff --git a/src/smoke/java/org/polaris2023/wildwind/smoke/InstallationTests.java b/src/smoke/java/org/polaris2023/wildwind/smoke/InstallationTests.java
new file mode 100644
index 0000000..c0fd676
--- /dev/null
+++ b/src/smoke/java/org/polaris2023/wildwind/smoke/InstallationTests.java
@@ -0,0 +1,61 @@
+package org.polaris2023.wildwind.smoke;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.Identifier;
+import net.minecraft.world.item.CreativeModeTab;
+import net.neoforged.bus.api.IEventBus;
+import net.neoforged.fml.ModList;
+import net.neoforged.fml.common.Mod;
+import net.neoforged.neoforge.registries.DeferredRegister;
+import org.polaris2023.wildwind.core.registry.ModCreativeTabs;
+
+/** Boots each supported installation using its actual release JAR dependencies. */
+@Mod("wild_wind_smoke")
+public final class InstallationTests {
+ public InstallationTests(IEventBus bus) {
+ var functions = DeferredRegister.>create(Registries.TEST_FUNCTION, "wild_wind_smoke");
+ functions.register("installation", () -> InstallationTests::installation);
+ functions.register(bus);
+ }
+
+ private static void check(GameTestHelper h, boolean condition, String message) {
+ h.assertTrue(condition, Component.literal(message));
+ }
+
+ private static void installation(GameTestHelper h) {
+ Set expected = Arrays.stream(System.getProperty("wildwind.testModules").split(",")).collect(Collectors.toSet());
+ for (String module : new String[]{"core", "wilderness", "barren"}) {
+ check(h, ModList.get().isLoaded("wild_wind_" + module) == expected.contains(module), "Loaded module: " + module);
+ }
+ check(h, ModList.get().isLoaded("geckolib") == expected.contains("wilderness"), "Only Wilderness needs GeckoLib");
+ check(h, !ModList.get().isLoaded("ww_hfas") && !ModList.get().isLoaded("ww_structures"), "Legacy entrypoints must not load");
+ var tab = ModCreativeTabs.WILD_WIND.get();
+ tab.buildContents(new CreativeModeTab.ItemDisplayParameters(h.getLevel().enabledFeatures(), true, h.getLevel().registryAccess()));
+ for (String path : new String[]{"azalea_planks", "azalea_sign", "azalea_hanging_sign", "azalea_shelf", "kiln", "duckweed"}) {
+ check(h, tab.contains(BuiltInRegistries.ITEM.getValue(Identifier.parse("wild_wind_core:" + path)).getDefaultInstance()), "Core item in tab: " + path);
+ }
+ for (String path : new String[]{"blaze_planks", "soul_planks", "scorched_dirt", "muddy_pig_spawn_egg"}) {
+ var id = Identifier.parse("wild_wind_wilderness:" + path);
+ check(h, BuiltInRegistries.ITEM.containsKey(id) == expected.contains("wilderness"), "Wilderness ownership: " + path);
+ if (expected.contains("wilderness")) check(h, tab.contains(BuiltInRegistries.ITEM.getValue(id).getDefaultInstance()), "Wilderness tab entry: " + path);
+ }
+ check(h, BuiltInRegistries.BLOCK.containsKey(Identifier.parse("wild_wind_barren:tiny_cactus")) == expected.contains("barren"), "Cactus belongs only to Barren");
+ check(h, BuiltInRegistries.BLOCK.keySet().stream().noneMatch(id -> id.getNamespace().startsWith("wild_wind_") && id.getPath().equals("fletching_table")), "Custom fletching table removed");
+ var inspectionPos = h.absolutePos(new net.minecraft.core.BlockPos(2, 2, 2));
+ h.getLevel().setBlockAndUpdate(inspectionPos, net.minecraft.world.level.block.Blocks.OAK_PLANKS.defaultBlockState());
+ var report = org.polaris2023.wildwind.core.inspection.InspectionReport.block(h.getLevel(), inspectionPos);
+ check(h, report.contains("可再生:是") && report.contains("硫方怪原型:弹性(橡胶球)"), "Inspector works with only Core and its own tags");
+ var book = new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.WRITABLE_BOOK);
+ var signed = org.polaris2023.wildwind.core.inspection.BookInspectionHandler.sign(book,
+ org.polaris2023.wildwind.core.inspection.BookPages.paginate(report), Component.literal("Oak Planks(minecraft:oak_planks)"), "test");
+ check(h, signed.is(net.minecraft.world.item.Items.WRITTEN_BOOK), "Core can persist complete signed inspection report");
+ h.succeed();
+ }
+}
diff --git a/src/smoke/resources/data/wild_wind_smoke/structure/empty.nbt b/src/smoke/resources/data/wild_wind_smoke/structure/empty.nbt
new file mode 100644
index 0000000..1b63d36
Binary files /dev/null and b/src/smoke/resources/data/wild_wind_smoke/structure/empty.nbt differ
diff --git a/src/smoke/resources/data/wild_wind_smoke/test_instance/installation.json b/src/smoke/resources/data/wild_wind_smoke/test_instance/installation.json
new file mode 100644
index 0000000..182f139
--- /dev/null
+++ b/src/smoke/resources/data/wild_wind_smoke/test_instance/installation.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_smoke:installation",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_smoke:empty",
+ "max_ticks": 100
+}
\ No newline at end of file
diff --git a/src/templates/META-INF/neoforge.mods.toml b/src/templates/META-INF/neoforge.mods.toml
index 7986c64..5efef7e 100644
--- a/src/templates/META-INF/neoforge.mods.toml
+++ b/src/templates/META-INF/neoforge.mods.toml
@@ -1,96 +1,37 @@
-# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods.
-# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
-# The overall format is standard TOML format, v0.5.0.
-# Note that there are a couple of TOML lists in this file.
-# Find more information on toml format here: https://github.com/toml-lang/toml
-
-# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
-# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license="${mod_license}"
-
-# A URL to refer people to when problems occur with this mod
-#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
-
-# A list of mods - how many allowed here is determined by the individual mod loader
-[[mods]] #mandatory
-
-# The modid of the mod
-modId="${mod_id}" #mandatory
-
-# The version number of the mod
-version="${mod_version}" #mandatory
-
-# A display name for the mod
-displayName="${mod_name}" #mandatory
-
-# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/
-#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
-
-# A URL for the "homepage" for this mod, displayed in the mod UI
-#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
-
-# A file name (in the root of the mod JAR) containing a logo for display
-#logoFile="examplemod.png" #optional
-
-# A text field displayed in the mod UI
-#credits="" #optional
-
-# The authors of the mod, displayed in the mod UI (optional)
+[[mods]]
+modId="${mod_id}"
+version="${mod_version}"
+displayName="${mod_name}"
authors="baka4n,viola-Siemens,ENC-Euphony,wdog5734"
+description='''${mod_name} for Minecraft 26.2.'''
-# The description text for the mod (multi line!) (#mandatory)
-description='''
-Example mod description.
-'''
-
-# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded.
-[[mixins]]
-config="${mod_id}.mixins.json"
-
-# The [[accessTransformers]] block allows you to declare where your AT file is.
-# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg
-#[[accessTransformers]]
-#file="META-INF/accesstransformer.cfg"
+${module_mixins}
-# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json
+[[dependencies.${mod_id}]]
+modId="neoforge"
+type="required"
+versionRange="[${neo_version},)"
+ordering="NONE"
+side="BOTH"
+[[dependencies.${mod_id}]]
+modId="minecraft"
+type="required"
+versionRange="${minecraft_version_range}"
+ordering="NONE"
+side="BOTH"
-# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
-[[dependencies.${mod_id}]] #optional
- # the modid of the dependency
- modId="neoforge" #mandatory
- # The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive).
- # 'required' requires the mod to exist, 'optional' does not
- # 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning
- type="required" #mandatory
- # Optional field describing why the dependency is required or why it is incompatible
- # reason="..."
- # The version range of the dependency
- versionRange="[${neo_version},)" #mandatory
- # An ordering relationship for the dependency.
- # BEFORE - This mod is loaded BEFORE the dependency
- # AFTER - This mod is loaded AFTER the dependency
- ordering="NONE"
- # Side this dependency is applied on - BOTH, CLIENT, or SERVER
- side="BOTH"
+${module_dependencies}
-# Here's another dependency
[[dependencies.${mod_id}]]
- modId="minecraft"
- type="required"
- # This version range declares a minimum of the current minecraft version up to but not including the next major version
- versionRange="${minecraft_version_range}"
- ordering="NONE"
- side="BOTH"
-
+modId="ww_hfas"
+type="incompatible"
+reason="Replace the old unified Wild Wind mod with the modular JARs."
+ordering="NONE"
+side="BOTH"
[[dependencies.${mod_id}]]
- modId="geckolib"
- type="required"
- versionRange="[${geocklib_version},)"
- ordering="NONE"
- side="BOTH"
-
-# Features are specific properties of the game environment, that you may want to declare you require. This example declares
-# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
-# stop your mod loading on the server for example.
-#[features.${mod_id}]
-#openGLVersion="[3.2,)"
+modId="ww_structures"
+type="incompatible"
+reason="Structure generation is included in Wild Wind:Core."
+ordering="NONE"
+side="BOTH"
diff --git a/src/verification/config/ww_structures/definitions/blocked.json b/src/verification/config/ww_structures/definitions/blocked.json
new file mode 100644
index 0000000..4c0bbc2
--- /dev/null
+++ b/src/verification/config/ww_structures/definitions/blocked.json
@@ -0,0 +1,7 @@
+{
+ "name": "blocked",
+ "template": "example_ruins.nbt",
+ "biomes": ["minecraft:plains"],
+ "placement": {"spacing": 2, "separation": 1, "salt": 123456},
+ "conditions": {"min_y": 1000, "max_y": 1001}
+}
diff --git a/src/verification/config/ww_structures/definitions/disabled.json b/src/verification/config/ww_structures/definitions/disabled.json
new file mode 100644
index 0000000..6997a49
--- /dev/null
+++ b/src/verification/config/ww_structures/definitions/disabled.json
@@ -0,0 +1,6 @@
+{
+ "name": "disabled",
+ "enabled": false,
+ "template": "example_ruins.nbt",
+ "biomes": ["minecraft:plains"]
+}
diff --git a/src/verification/config/ww_structures/definitions/smoke.json b/src/verification/config/ww_structures/definitions/smoke.json
new file mode 100644
index 0000000..b66cc37
--- /dev/null
+++ b/src/verification/config/ww_structures/definitions/smoke.json
@@ -0,0 +1,9 @@
+{
+ "name": "smoke",
+ "template": "wide_ruins.nbt",
+ "biomes": ["minecraft:plains"],
+ "placement": {"spacing": 2, "separation": 1, "salt": 142857, "frequency": 1.0},
+ "y_offset": -1,
+ "rotation": "clockwise_90",
+ "conditions": {"min_y": -2032, "max_y": 2031, "max_terrain_difference": 0, "avoid_fluids": true, "ground_blocks": ["#minecraft:dirt", "minecraft:grass_block"]}
+}
diff --git a/src/verification/config/ww_structures/templates/wide_ruins.nbt b/src/verification/config/ww_structures/templates/wide_ruins.nbt
new file mode 100644
index 0000000..66d00f6
Binary files /dev/null and b/src/verification/config/ww_structures/templates/wide_ruins.nbt differ
diff --git a/src/verification/java/org/polaris2023/wildwind/hfas/test/AzaleaAndArrowGameTests.java b/src/verification/java/org/polaris2023/wildwind/hfas/test/AzaleaAndArrowGameTests.java
new file mode 100644
index 0000000..827e697
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/hfas/test/AzaleaAndArrowGameTests.java
@@ -0,0 +1,241 @@
+package org.polaris2023.wildwind.hfas.test;
+
+import java.util.ArrayList;
+import java.util.List;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.Identifier;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.tags.ItemTags;
+import net.minecraft.tags.TagKey;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.entity.EntitySpawnReason;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.projectile.arrow.AbstractArrow;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.DyeColor;
+import net.minecraft.world.item.alchemy.PotionContents;
+import net.minecraft.world.item.alchemy.Potions;
+import net.minecraft.world.item.crafting.CraftingInput;
+import net.minecraft.world.item.crafting.RecipeType;
+import net.minecraft.world.level.GameType;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.ShelfBlock;
+import net.minecraft.world.level.block.DispenserBlock;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.ShelfBlockEntity;
+import net.minecraft.world.level.block.entity.SignBlockEntity;
+import net.minecraft.world.level.block.entity.SignText;
+import net.minecraft.world.level.block.entity.HopperBlockEntity;
+import net.minecraft.world.level.block.state.properties.BlockStateProperties;
+import net.minecraft.world.level.storage.loot.LootParams;
+import net.minecraft.world.level.storage.loot.LootContext;
+import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
+import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
+import net.minecraft.world.phys.BlockHitResult;
+import net.minecraft.world.phys.Vec3;
+import org.polaris2023.wildwind.core.CoreMod;
+import org.polaris2023.wildwind.wilderness.entity.ReachArrow;
+import org.polaris2023.wildwind.wilderness.registry.ModEntities;
+import org.polaris2023.wildwind.wilderness.registry.ModMobEffects;
+import org.polaris2023.wildwind.wilderness.registry.ModPotions;
+
+final class AzaleaAndArrowGameTests {
+ private static void check(GameTestHelper h, boolean value, String message) {
+ h.assertTrue(value, Component.literal(message));
+ }
+
+ static void signs(GameTestHelper h) {
+ var level = h.getLevel();
+ var blocks = List.of(org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_SIGN.get(), org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_WALL_SIGN.get(),
+ org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_HANGING_SIGN.get(), org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_WALL_HANGING_SIGN.get());
+ int x = 1;
+ for (var block : blocks) {
+ var rel = new BlockPos(x++, 2, 2);
+ var pos = h.absolutePos(rel);
+ var state = block.defaultBlockState();
+ level.setBlock(pos, state, 2);
+ check(h, level.getBlockEntity(pos) instanceof SignBlockEntity, "All sign variants must create native sign entities");
+ var sign = (SignBlockEntity) level.getBlockEntity(pos);
+ check(h, sign.getType().onlyOpCanSetNbt(), "Sign custom data must retain native administrator-only protection");
+ check(h, sign.getType().isValid(state), "Native sign type must accept azalea blocks");
+ sign.setText(new SignText().setMessage(0, Component.literal("Front")).setColor(DyeColor.RED).setHasGlowingText(true), true);
+ sign.setText(new SignText().setMessage(3, Component.literal("Back")), false);
+ sign.setWaxed(true);
+ var saved = sign.saveWithFullMetadata(level.registryAccess());
+ var loaded = (SignBlockEntity) BlockEntity.loadStatic(pos, state, saved, level.registryAccess());
+ check(h, loaded != null && loaded.isWaxed() && loaded.getFrontText().hasGlowingText()
+ && loaded.getFrontText().getColor() == DyeColor.RED
+ && loaded.getBackText().getMessage(3, false).getString().equals("Back"), "Both faces, dye, glow and wax must survive saving");
+ check(h, state.getPistonPushReaction() == net.minecraft.world.level.material.PushReaction.BLOCK, "Signs must block pistons");
+ check(h, state.getDestroySpeed(level, pos) == 1 && block.getExplosionResistance() == 1, "Sign hardness/resistance must be one");
+ check(h, state.getFlammability(level, pos, Direction.UP) == 0, "Signs must not burn in fire");
+ check(h, !Block.getDrops(state, level, pos, sign).isEmpty(), "Every sign variant must drop its item");
+ check(h, state.hasProperty(BlockStateProperties.WATERLOGGED), "Signs must support waterlogging");
+ }
+ check(h, org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_SIGN.get().asItem().getDefaultInstance().getMaxStackSize() == 16
+ && org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_HANGING_SIGN.get().asItem().getDefaultInstance().getMaxStackSize() == 16, "Signs stack to 16");
+ check(h, new ItemStack(org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_SIGN.get()).getBurnTime(null, level.fuelValues()) == 200
+ && new ItemStack(org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_HANGING_SIGN.get()).getBurnTime(null, level.fuelValues()) == 800, "Sign fuel values must match specification");
+ h.succeed();
+ }
+
+ static void shelf(GameTestHelper h) {
+ var level = h.getLevel();
+ var rel = new BlockPos(3, 2, 3);
+ var pos = h.absolutePos(rel);
+ var state = org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_SHELF.get().defaultBlockState().setValue(ShelfBlock.FACING, Direction.NORTH);
+ level.setBlock(pos, state, 3);
+ var shelf = (ShelfBlockEntity) level.getBlockEntity(pos);
+ check(h, shelf != null && shelf.getType().isValid(state), "Shelf must use a valid native container");
+ check(h, !shelf.getAlignItemsToBottom(), "Shelf display alignment must default to centered");
+ for (int mask = 0; mask < 8; mask++) {
+ for (int slot = 0; slot < 3; slot++) shelf.setItem(slot, (mask & 1 << slot) == 0 ? ItemStack.EMPTY : new ItemStack(Items.DIAMOND, 64));
+ check(h, state.getAnalogOutputSignal(level, pos, Direction.SOUTH) == mask, "Shelf comparator must encode all eight occupancy patterns");
+ check(h, state.getAnalogOutputSignal(level, pos, Direction.NORTH) == 0, "Comparator only reads from the rear");
+ }
+ shelf.clearContent();
+ var player = h.makeMockPlayer(GameType.SURVIVAL);
+ player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.EMERALD, 32));
+ h.useBlock(rel, player, new BlockHitResult(new Vec3(pos.getX() + 0.8, pos.getY() + 0.5, pos.getZ()), Direction.NORTH, pos, false));
+ check(h, shelf.getItem(0).is(Items.EMERALD) && shelf.getItem(0).getCount() == 32 && player.getMainHandItem().isEmpty(), "Clicking left slot must swap the entire main-hand stack");
+ player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.DIAMOND, 7));
+ h.useBlock(rel, player, new BlockHitResult(new Vec3(pos.getX() + 0.8, pos.getY() + 0.5, pos.getZ()), Direction.NORTH, pos, false));
+ check(h, shelf.getItem(0).is(Items.DIAMOND) && player.getMainHandItem().is(Items.EMERALD), "Occupied slot must return its previous stack");
+ var saved = shelf.saveWithFullMetadata(level.registryAccess());
+ saved.putBoolean("align_items_to_bottom", true);
+ var loaded = (ShelfBlockEntity) BlockEntity.loadStatic(pos, state, saved, level.registryAccess());
+ check(h, loaded != null && loaded.getAlignItemsToBottom() && loaded.getItem(0).getCount() == 7, "Alignment and contents must persist");
+ h.setBlock(3, 1, 3, Blocks.HOPPER);
+ var hopper = (HopperBlockEntity) level.getBlockEntity(pos.below());
+ check(h, HopperBlockEntity.suckInItems(level, hopper) && hopper.getItem(0).is(Items.DIAMOND), "Hopper must extract leftmost occupied slot");
+ check(h, level.getCapability(net.neoforged.neoforge.capabilities.Capabilities.Item.BLOCK, pos, Direction.UP) != null, "Shelf must expose native item capability");
+ check(h, state.getFlammability(level, pos, Direction.UP) == 20 && state.getFireSpreadSpeed(level, pos, Direction.UP) == 30, "Shelf fire values must be 30/20");
+ check(h, new ItemStack(org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_SHELF.get()).getBurnTime(null, level.fuelValues()) == 300, "Shelf fuel value must be 300");
+ h.succeed();
+ }
+
+ static void shelfChain(GameTestHelper h) {
+ var level = h.getLevel();
+ for (int count = 1; count <= 3; count++) {
+ for (int x = 1; x <= 4; x++) {
+ h.setBlock(x, 2, 3, Blocks.AIR);
+ h.setBlock(x, 1, 3, Blocks.AIR);
+ }
+ for (int x = 1; x <= count; x++) {
+ h.setBlock(x, 1, 3, Blocks.REDSTONE_BLOCK);
+ h.setBlock(x, 2, 3, org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_SHELF.get().defaultBlockState()
+ .setValue(ShelfBlock.FACING, Direction.NORTH).setValue(ShelfBlock.POWERED, true));
+ }
+ var rel = new BlockPos(1, 2, 3);
+ var pos = h.absolutePos(rel);
+ check(h, org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_SHELF.get().getAllBlocksConnectedTo(level, pos).size() == count, "Powered shelf chain must connect up to three shelves");
+ var player = h.makeMockPlayer(GameType.SURVIVAL);
+ for (int slot = 0; slot < 9; slot++) player.getInventory().setItem(slot, new ItemStack(Items.DIAMOND, slot + 1));
+ h.useBlock(rel, player, new BlockHitResult(new Vec3(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ()), Direction.NORTH, pos, false));
+ for (int slot = 0; slot < 9; slot++) check(h, player.getInventory().getItem(slot).isEmpty() == (slot >= 9 - 3 * count), "Powered shelves must swap rightmost 3/6/9 hotbar slots");
+ h.useBlock(rel, player, new BlockHitResult(new Vec3(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ()), Direction.NORTH, pos, false));
+ for (int slot = 0; slot < 9; slot++) check(h, player.getInventory().getItem(slot).getCount() == slot + 1, "Second swap must restore exact hotbar stacks");
+ }
+ h.succeed();
+ }
+
+ static void arrows(GameTestHelper h) {
+ var level = h.getLevel();
+ var owner = h.makeMockPlayer(GameType.SURVIVAL);
+ var target = EntityTypes.COW.create(level, EntitySpawnReason.COMMAND);
+ for (var potion : List.of(ModPotions.REACH, ModPotions.LONG_REACH)) {
+ var slots = new ArrayList();
+ for (int i = 0; i < 9; i++) slots.add(new ItemStack(Items.ARROW));
+ slots.set(4, PotionContents.createItemStack(Items.LINGERING_POTION, potion));
+ var input = CraftingInput.of(3, 3, slots);
+ var recipe = level.recipeAccess().getRecipeFor(RecipeType.CRAFTING, input, level).orElseThrow();
+ var result = recipe.value().assemble(input);
+ check(h, result.is(org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get()) && result.getCount() == 8, "Vanilla surround recipe must produce eight mod Reach arrows");
+ check(h, result.is(ItemTags.ARROWS), "Reach arrows must be valid bow and crossbow ammunition");
+ var arrow = org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get().createArrow(level, result, owner, new ItemStack(Items.BOW));
+ check(h, arrow.getType() == ModEntities.REACH_ARROW.get() && arrow.getOwner() == owner, "Bow must spawn the custom arrow type with owner");
+ check(h, !org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get().isInfinite(result, new ItemStack(Items.BOW), owner), "Infinity must not save potion arrows");
+ var hitArrow = new TestArrow(level, h.absoluteVec(new Vec3(2.5, 2.5, 2.5)), result);
+ target.removeAllEffects();
+ hitArrow.hit(target);
+ int expected = potion == ModPotions.REACH ? 450 : 1200;
+ check(h, target.getEffect(ModMobEffects.REACH).getDuration() == expected, "Arrow must apply exactly 450/1200 ticks of Reach");
+ h.setBlock(2, 2, 2, Blocks.STONE);
+ for (int tick = 0; tick < 599; tick++) hitArrow.tick();
+ check(h, hitArrow.getPickupItemStackOrigin().is(org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get()), "Potion must remain before 600 ground ticks");
+ hitArrow.tick();
+ check(h, hitArrow.getPickupItemStackOrigin().is(Items.ARROW) && hitArrow.getColor() == -1, "At 600 ground ticks pickup must become an ordinary arrow");
+ }
+ check(h, DispenserBlock.DISPENSER_REGISTRY.containsKey(org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get()), "Dispenser projectile behavior must be registered");
+ var dispensed = org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get().asProjectile(level, Vec3.ZERO, new ItemStack(org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get()), Direction.NORTH);
+ check(h, dispensed.getType() == ModEntities.REACH_ARROW.get() && ((AbstractArrow) dispensed).pickup == AbstractArrow.Pickup.ALLOWED, "Dispensed arrows must be recoverable custom arrows");
+ var poison = PotionContents.createItemStack(Items.TIPPED_ARROW, Potions.POISON);
+ check(h, org.polaris2023.wildwind.wilderness.arrow.ReachArrows.convert(poison) == poison, "Other potion arrows must remain vanilla");
+ h.succeed();
+ }
+
+ static void acquisition(GameTestHelper h) {
+ var level = h.getLevel();
+ var villager = EntityTypes.VILLAGER.create(level, EntitySpawnReason.COMMAND);
+ var tag = TagKey.create(Registries.POTION, Identifier.withDefaultNamespace("tradeable"));
+ var potions = level.registryAccess().lookupOrThrow(Registries.POTION).getOrThrow(tag);
+ check(h, potions.contains(ModPotions.REACH) && potions.contains(ModPotions.LONG_REACH), "Both Reach variants must join the survival potion trade pool");
+ var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, Vec3.ZERO)
+ .withParameter(LootContextParams.THIS_ENTITY, villager)
+ .withParameter(LootContextParams.ADDITIONAL_COST_COMPONENT_ALLOWED, net.minecraft.util.Unit.INSTANCE)
+ .create(LootContextParamSets.VILLAGER_TRADE);
+ var context = new LootContext.Builder(params).withOptionalRandomSeed(271828L).create(java.util.Optional.empty());
+ var trades = level.registryAccess().lookupOrThrow(Registries.VILLAGER_TRADE);
+ var trade = trades.getOrThrow(ResourceKey.create(Registries.VILLAGER_TRADE,
+ Identifier.withDefaultNamespace("fletcher/5/arrow_and_emerald_tipped_arrow"))).value();
+ var set = level.registryAccess().lookupOrThrow(Registries.TRADE_SET).getOrThrow(ResourceKey.create(Registries.TRADE_SET,
+ Identifier.withDefaultNamespace("fletcher/level_5"))).value();
+ check(h, set.calculateNumberOfTrades(context) == 2 && set.getTrades().size() == 3 && !set.allowDuplicates(),
+ "Master fletcher must keep vanilla two-of-three trade selection");
+ boolean normal = false, longReach = false, other = false;
+ for (int i = 0; i < 2048; i++) {
+ var offer = trade.getOffer(context);
+ check(h, offer != null && offer.getBaseCostA().is(Items.EMERALD) && offer.getBaseCostA().getCount() == 2
+ && offer.getCostB().is(Items.ARROW) && offer.getCostB().getCount() == 5 && offer.getResult().getCount() == 5,
+ "Fletcher potion-arrow cost must remain two emeralds plus five arrows for five arrows");
+ var result = offer.getResult();
+ if (result.is(org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get())) {
+ var potion = result.get(DataComponents.POTION_CONTENTS).potion().orElseThrow();
+ normal |= potion.is(ModPotions.REACH.getKey());
+ longReach |= potion.is(ModPotions.LONG_REACH.getKey());
+ } else other |= result.is(Items.TIPPED_ARROW);
+ }
+ check(h, normal && longReach && other, "Random trade must offer both custom Reach arrows and vanilla alternatives");
+ var gift = level.getServer().reloadableRegistries().getLootTable(ResourceKey.create(Registries.LOOT_TABLE,
+ Identifier.withDefaultNamespace("gameplay/hero_of_the_village/fletcher_gift")));
+ var giftParams = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, Vec3.ZERO)
+ .withParameter(LootContextParams.THIS_ENTITY, villager).create(LootContextParamSets.GIFT);
+ var random = net.minecraft.util.RandomSource.create(314159L);
+ boolean reachGift = false, vanillaGift = false;
+ for (int i = 0; i < 1024; i++) {
+ var results = gift.getRandomItems(giftParams, random);
+ check(h, results.size() <= 1, "Reach gift must join the existing random choice, not add an extra gift");
+ for (var result : results) {
+ reachGift |= result.is(org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get());
+ vanillaGift |= result.is(Items.ARROW) || result.is(Items.TIPPED_ARROW);
+ }
+ }
+ check(h, reachGift && vanillaGift, "Hero gifts must include Reach and preserve vanilla choices");
+ h.succeed();
+ }
+
+ private static class TestArrow extends ReachArrow {
+ TestArrow(Level level, Vec3 pos, ItemStack stack) { super(level, pos.x, pos.y, pos.z, stack, null); }
+ void hit(LivingEntity target) { doPostHurtEffects(target); }
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/hfas/test/BookInspectionGameTests.java b/src/verification/java/org/polaris2023/wildwind/hfas/test/BookInspectionGameTests.java
new file mode 100644
index 0000000..43b1642
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/hfas/test/BookInspectionGameTests.java
@@ -0,0 +1,169 @@
+package org.polaris2023.wildwind.hfas.test;
+
+import java.util.List;
+import java.util.stream.IntStream;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.network.Filterable;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.InteractionResult;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.entity.ai.attributes.Attributes;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.component.WritableBookContent;
+import net.minecraft.world.level.GameType;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.piston.PistonBaseBlock;
+import net.minecraft.world.phys.BlockHitResult;
+import net.minecraft.world.phys.Vec3;
+import net.neoforged.neoforge.common.NeoForge;
+import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
+import org.polaris2023.wildwind.core.inspection.BookInspectionHandler;
+import org.polaris2023.wildwind.core.inspection.InspectionReport;
+import org.polaris2023.wildwind.core.inspection.SulfurProfile;
+
+public final class BookInspectionGameTests {
+ private static void check(GameTestHelper h, boolean ok, String message) { h.assertTrue(ok, Component.literal(message)); }
+ private static void field(GameTestHelper h, List report, String row) { check(h, report.contains(row), "Missing report row: " + row + " in " + report); }
+
+ static void blocks(GameTestHelper h) {
+ var level = h.getLevel();
+ var pos = h.absolutePos(new BlockPos(2, 2, 2));
+ level.setBlockAndUpdate(pos, Blocks.OAK_PLANKS.defaultBlockState());
+ var report = InspectionReport.block(level, pos);
+ for (String row : List.of("方块ID:minecraft:oak_planks", "硬度:2", "爆炸抗性:3", "引燃几率:5", "烧毁几率:20",
+ "熔岩可燃性:是", "乐器:贝斯", "烧炼时间:15秒(300刻)", "最大堆叠:64", "可再生:是", "物品ID:minecraft:oak_planks")) field(h, report, row);
+ check(h, report.stream().anyMatch(row -> row.startsWith("合适挖掘工具:") && row.contains("斧")), "Planks use axes");
+ level.setBlockAndUpdate(pos, Blocks.COBBLESTONE.defaultBlockState());
+ report = InspectionReport.block(level, pos);
+ field(h, report, "引燃几率:×");
+ field(h, report, "烧炼时间:×");
+ field(h, report, "烧炼加工时间:10秒(200刻)");
+ level.setBlockAndUpdate(pos, Blocks.WATER.defaultBlockState());
+ field(h, InspectionReport.block(level, pos), "物品ID:×");
+ field(h, InspectionReport.block(level, pos), "最大堆叠:×");
+ level.setBlockAndUpdate(pos, Blocks.DIAMOND_BLOCK.defaultBlockState());
+ field(h, InspectionReport.block(level, pos), "可再生:否");
+ level.setBlockAndUpdate(pos, Blocks.BARRIER.defaultBlockState());
+ field(h, InspectionReport.block(level, pos), "可再生:×");
+ level.setBlockAndUpdate(pos, org.polaris2023.wildwind.core.block.ModBlocks.AZALEA_PLANKS.get().defaultBlockState());
+ report = InspectionReport.block(level, pos);
+ field(h, report, "方块ID:wild_wind_core:azalea_planks");
+ field(h, report, "可再生:是");
+ field(h, report, "烧炼时间:15秒(300刻)");
+ for (var block : List.of(Blocks.OBSIDIAN, Blocks.CHEST, Blocks.BEDROCK)) {
+ check(h, InspectionReport.piston(block.defaultBlockState(), level, pos).equals("不可推动,不可拉动"), "Special immovable block: " + block);
+ }
+ check(h, InspectionReport.piston(Blocks.STONE.defaultBlockState(), level, pos).equals("可推动,可拉动"), "Stone is movable");
+ check(h, InspectionReport.piston(Blocks.TORCH.defaultBlockState(), level, pos).equals("被推动破坏,不可拉动"), "Torch breaks when pushed");
+ check(h, InspectionReport.piston(Blocks.GLAZED_TERRACOTTA.white().defaultBlockState(), level, pos).equals("可推动,不可拉动"), "Glazed terracotta push-only");
+ check(h, InspectionReport.piston(Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.EXTENDED, true), level, pos).equals("不可推动,不可拉动"), "Extended piston immovable");
+ h.succeed();
+ }
+
+ static void interactions(GameTestHelper h) {
+ var player = h.makeMockServerPlayerInLevel();
+ var pos = h.absolutePos(new BlockPos(2, 2, 2));
+ h.getLevel().setBlockAndUpdate(pos, Blocks.CHEST.defaultBlockState());
+ var book = new ItemStack(Items.WRITABLE_BOOK);
+ var note = new Filterable<>("已有笔记,请保留", java.util.Optional.of("过滤后的笔记"));
+ book.set(DataComponents.WRITABLE_BOOK_CONTENT, new WritableBookContent(List.of(note)));
+ player.setItemInHand(InteractionHand.MAIN_HAND, book);
+ var hit = new BlockHitResult(Vec3.atCenterOf(pos), Direction.UP, pos, false);
+ var normal = NeoForge.EVENT_BUS.post(new PlayerInteractEvent.RightClickBlock(player, InteractionHand.MAIN_HAND, pos, hit));
+ check(h, !normal.isCanceled() && book.get(DataComponents.WRITABLE_BOOK_CONTENT).pages().size() == 1, "Normal use must remain unchanged");
+ player.setShiftKeyDown(true);
+ var event = NeoForge.EVENT_BUS.post(new PlayerInteractEvent.RightClickBlock(player, InteractionHand.MAIN_HAND, pos, hit));
+ check(h, event.isCanceled() && event.getCancellationResult() == InteractionResult.SUCCESS, "Inspector consumes interaction before opening chest/book");
+ var signed = player.getItemInHand(InteractionHand.MAIN_HAND);
+ check(h, signed.is(Items.WRITTEN_BOOK) && !signed.has(DataComponents.WRITABLE_BOOK_CONTENT), "Held book becomes signed book without editable pages");
+ var content = signed.get(DataComponents.WRITTEN_BOOK_CONTENT);
+ var pages = content.pages();
+ check(h, pages.size() > 1 && pages.getFirst().equals(note.map(Component::literal)), "Signing preserves raw and filtered notes");
+ check(h, content.author().equals(player.getGameProfile().name()) && content.generation() == 0 && content.resolved(), "Player is author of original literal snapshot");
+ String chestName = Blocks.CHEST.getName().getString() + "(minecraft:chest)";
+ check(h, content.title().raw().equals(chestName) && signed.getHoverName().getString().equals(chestName), "Signed title includes block name and ID");
+ check(h, pages.stream().allMatch(p -> p.raw().getString().length() <= 1024 && p.raw().getString().split("\n", -1).length <= 12), "Pages fit book layout limits");
+ var repeated = NeoForge.EVENT_BUS.post(new PlayerInteractEvent.RightClickBlock(player, InteractionHand.MAIN_HAND, pos, hit));
+ check(h, !repeated.isCanceled() && player.getItemInHand(InteractionHand.MAIN_HAND) == signed, "Signed book is never inspected again");
+ var full = new WritableBookContent(IntStream.range(0, 100).mapToObj(i -> Filterable.passThrough("note " + i)).toList());
+ book.set(DataComponents.WRITABLE_BOOK_CONTENT, full);
+ player.setItemInHand(InteractionHand.MAIN_HAND, book);
+ NeoForge.EVENT_BUS.post(new PlayerInteractEvent.RightClickBlock(player, InteractionHand.MAIN_HAND, pos, hit));
+ check(h, player.getItemInHand(InteractionHand.MAIN_HAND) == book && book.get(DataComponents.WRITABLE_BOOK_CONTENT).equals(full), "Full held book is unchanged");
+ var almostFull = new WritableBookContent(full.pages().subList(0, 99));
+ book.set(DataComponents.WRITABLE_BOOK_CONTENT, almostFull);
+ check(h, BookInspectionHandler.sign(book, List.of("one", "two"), Component.literal("test"), "author").isEmpty()
+ && book.get(DataComponents.WRITABLE_BOOK_CONTENT).equals(almostFull), "Insufficient space never partially appends a report");
+ player.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
+ var offhand = new ItemStack(Items.WRITABLE_BOOK);
+ player.setItemInHand(InteractionHand.OFF_HAND, offhand);
+ var cow = h.spawn(EntityTypes.COW, 3, 2, 3);
+ var entityEvent = NeoForge.EVENT_BUS.post(new PlayerInteractEvent.EntityInteract(player, InteractionHand.OFF_HAND, cow, Vec3.ZERO));
+ var signedEntity = player.getItemInHand(InteractionHand.OFF_HAND);
+ check(h, entityEvent.isCanceled() && signedEntity.is(Items.WRITTEN_BOOK)
+ && !signedEntity.get(DataComponents.WRITTEN_BOOK_CONTENT).pages().isEmpty()
+ && signedEntity.getHoverName().getString().equals(cow.getType().getDescription().getString() + "(minecraft:cow)")
+ && player.getItemInHand(InteractionHand.MAIN_HAND).isEmpty(), "Offhand entity interaction replaces only the offhand book");
+ var report = InspectionReport.entity(h.getLevel(), cow);
+ field(h, report, "实体ID:minecraft:cow"); field(h, report, "方块ID:×"); field(h, report, "硬度:×"); field(h, report, "最大生命值:10");
+ field(h, report, "物品ID:×"); field(h, report, "可再生:是");
+ var observer = h.makeMockServerPlayer(GameType.SPECTATOR);
+ observer.setShiftKeyDown(true);
+ observer.setItemInHand(InteractionHand.OFF_HAND, offhand);
+ var spectator = NeoForge.EVENT_BUS.post(new PlayerInteractEvent.EntityInteract(observer, InteractionHand.OFF_HAND, cow, Vec3.ZERO));
+ check(h, !spectator.isCanceled(), "Spectators never write");
+ var dropped = new net.minecraft.world.entity.item.ItemEntity(h.getLevel(), 0, 0, 0, new ItemStack(Items.COBBLESTONE, 12));
+ var itemReport = InspectionReport.entity(h.getLevel(), dropped);
+ field(h, itemReport, "实体ID:minecraft:item");
+ field(h, itemReport, "物品ID:minecraft:cobblestone");
+ field(h, itemReport, "最大堆叠:64");
+ field(h, itemReport, "生命值:×");
+ check(h, BookInspectionHandler.sign(signedEntity, List.of("overwrite"), Component.literal("test"), "author").isEmpty(), "Signed books cannot be changed");
+ var longName = Component.literal("杜鹃木悬挂式告示牌(wild_wind_core:azalea_hanging_sign)");
+ var longBook = BookInspectionHandler.sign(offhand, List.of("report"), longName, "author");
+ check(h, longBook.getHoverName().equals(longName), "Full name and long ID are preserved for display");
+ var longContent = longBook.get(DataComponents.WRITTEN_BOOK_CONTENT);
+ check(h, longContent.title().raw().length() <= 32
+ && net.minecraft.world.item.component.WrittenBookContent.CODEC.encodeStart(
+ h.getLevel().registryAccess().createSerializationContext(com.mojang.serialization.JsonOps.INSTANCE), longContent).isSuccess(),
+ "Long titles serialize within vanilla limits");
+ var unicodeBook = BookInspectionHandler.sign(offhand, List.of("report"), Component.literal("x".repeat(31) + "😀(minecraft:stone)"), "author");
+ check(h, unicodeBook.get(DataComponents.WRITTEN_BOOK_CONTENT).title().raw().equals("x".repeat(31)), "Title truncation preserves surrogate pairs");
+ h.succeed();
+ }
+
+ static void sulfur(GameTestHelper h) {
+ var level = h.getLevel();
+ var examples = List.of(Items.OAK_PLANKS, Items.MUD, Items.IRON_BLOCK, Items.MOSS_BLOCK, Items.WOOL.white(),
+ Items.PACKED_ICE, Items.RED_MUSHROOM_BLOCK, Items.STONE, Items.HONEYCOMB_BLOCK, Items.SOUL_SAND, Items.TNT, Items.MAGMA_BLOCK);
+ var names = List.of("弹性(橡胶球)", "普通(足球)", "缓慢平移(药球)", "快速平移(高尔夫球)", "轻盈(沙滩球)",
+ "快速滑行(冰球)", "缓慢滑行(冰壶)", "缓慢弹性", "黏性", "高阻力", "爆炸", "高温");
+ var cubes = new java.util.ArrayList();
+ for (int i = 0; i < examples.size(); i++) {
+ var stack = examples.get(i).getDefaultInstance();
+ var profile = SulfurProfile.of(level, stack);
+ check(h, profile.name().equals(names.get(i)), "Vanilla archetype membership for " + stack + ": " + profile.name());
+ var cube = h.spawn(EntityTypes.SULFUR_CUBE, 2, 2, 2);
+ cube.setNoAi(true);
+ cube.setItemSlot(EquipmentSlot.BODY, stack);
+ cubes.add(cube);
+ }
+ check(h, SulfurProfile.of(level, Items.STICK.getDefaultInstance()).name().equals("默认"), "Unmatched items use default archetype");
+ h.runAfterDelay(3, () -> {
+ for (var cube : cubes) {
+ var profile = SulfurProfile.of(level, cube.getItemBySlot(EquipmentSlot.BODY));
+ field(h, InspectionReport.entity(level, cube), "硫方怪原型:" + profile.name());
+ for (var attribute : List.of(Attributes.KNOCKBACK_RESISTANCE, Attributes.BOUNCINESS, Attributes.FRICTION_MODIFIER, Attributes.AIR_DRAG_MODIFIER)) {
+ check(h, Math.abs(profile.attribute(attribute) - cube.getAttributeValue(attribute)) < 0.0001, "Inspector physics agrees with actual equipped sulfur cube: " + profile.name());
+ }
+ }
+ h.succeed();
+ });
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/hfas/test/EnchantmentTableGameTests.java b/src/verification/java/org/polaris2023/wildwind/hfas/test/EnchantmentTableGameTests.java
new file mode 100644
index 0000000..ad82e89
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/hfas/test/EnchantmentTableGameTests.java
@@ -0,0 +1,66 @@
+package org.polaris2023.wildwind.hfas.test;
+
+import java.util.List;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.tags.EnchantmentTags;
+import net.minecraft.world.inventory.ContainerLevelAccess;
+import net.minecraft.world.inventory.EnchantmentMenu;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.enchantment.EnchantmentHelper;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.EnchantingTableBlock;
+import org.polaris2023.wildwind.core.enchantment.MeltingEnchantment;
+
+public final class EnchantmentTableGameTests {
+ private static void check(GameTestHelper h, boolean ok, String message) {
+ h.assertTrue(ok, Component.literal(message));
+ }
+
+ static void melting(GameTestHelper h) {
+ var level = h.getLevel();
+ var registry = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT);
+ var melting = registry.getOrThrow(MeltingEnchantment.MELTING);
+ var tablePool = registry.getOrThrow(EnchantmentTags.IN_ENCHANTING_TABLE);
+ check(h, tablePool.contains(melting), "Melting belongs to the real enchanting-table pool");
+ for (var item : List.of(Items.DIAMOND_CHESTPLATE, Items.DIAMOND_SWORD)) {
+ check(h, EnchantmentHelper.getAvailableEnchantmentResults(30, new ItemStack(item), tablePool.stream())
+ .stream().noneMatch(e -> e.enchantment().equals(melting)), "Melting is not offered for " + item);
+ }
+
+ var player = h.makeMockServerPlayerInLevel();
+ var pos = h.absolutePos(new BlockPos(3, 2, 3));
+ level.setBlockAndUpdate(pos, Blocks.ENCHANTING_TABLE.defaultBlockState());
+ for (var offset : EnchantingTableBlock.BOOKSHELF_OFFSETS) {
+ level.setBlockAndUpdate(pos.offset(offset), Blocks.BOOKSHELF.defaultBlockState());
+ }
+ int meltingId = registry.asHolderIdMap().getId(melting);
+ for (var item : List.of(Items.DIAMOND_PICKAXE, Items.DIAMOND_AXE, Items.DIAMOND_SHOVEL, Items.DIAMOND_HOE, Items.BOOK)) {
+ boolean obtained = false;
+ var menu = new EnchantmentMenu(1, player.getInventory(), ContainerLevelAccess.create(level, pos));
+ menu.getSlot(1).set(new ItemStack(Items.LAPIS_LAZULI, 64));
+ // Data slot 3 is vanilla's synchronized enchantment seed. Fixed seeds make the
+ // test reproducible while exercising real offers and the menu's apply path.
+ for (int seed = 0; seed < 2048 && !obtained; seed++) {
+ menu.setData(3, seed);
+ menu.getSlot(0).set(new ItemStack(item));
+ for (int slot = 0; slot < 3; slot++) {
+ if (menu.enchantClue[slot] != meltingId) continue;
+ check(h, menu.clickMenuButton(player, slot), "Enchanting-table offer can be selected");
+ var result = menu.getSlot(0).getItem();
+ var enchantments = result.get(item == Items.BOOK ? DataComponents.STORED_ENCHANTMENTS : DataComponents.ENCHANTMENTS);
+ check(h, enchantments != null && enchantments.getLevel(melting) == 1, "Table actually applies Melting I");
+ check(h, item != Items.BOOK || result.is(Items.ENCHANTED_BOOK), "Table transforms a normal book");
+ obtained = true;
+ break;
+ }
+ }
+ check(h, obtained, "Melting can be randomly obtained from the table on " + item);
+ }
+ h.succeed();
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/hfas/test/FluidEntityRulesGameTests.java b/src/verification/java/org/polaris2023/wildwind/hfas/test/FluidEntityRulesGameTests.java
new file mode 100644
index 0000000..fcc604e
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/hfas/test/FluidEntityRulesGameTests.java
@@ -0,0 +1,218 @@
+package org.polaris2023.wildwind.hfas.test;
+
+import java.util.List;
+import java.util.Set;
+import java.util.HashSet;
+import net.minecraft.tags.TagKey;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.tags.EntityTypeTags;
+import net.minecraft.tags.FluidTags;
+import net.minecraft.world.effect.MobEffectInstance;
+import net.minecraft.world.effect.MobEffects;
+import net.minecraft.world.entity.EntitySpawnReason;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.MoverType;
+import net.minecraft.world.level.GameType;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.phys.Vec3;
+import net.neoforged.neoforge.common.CommonHooks;
+import org.polaris2023.wildwind.wilderness.effect.FluidEffects;
+import org.polaris2023.wildwind.wilderness.effect.ModEffects;
+import org.polaris2023.wildwind.wilderness.entity.ModEntityTypeTags;
+
+final class FluidEntityRulesGameTests {
+ private FluidEntityRulesGameTests() {}
+
+ private static void check(GameTestHelper h, boolean condition, String message) {
+ h.assertTrue(condition, Component.literal(message));
+ }
+
+ private static LivingEntity submerged(GameTestHelper h, EntityType> type) {
+ var entity = type == EntityTypes.PLAYER ? h.makeMockServerPlayer(GameType.SURVIVAL)
+ : (LivingEntity) type.create(h.getLevel(), EntitySpawnReason.COMMAND);
+ check(h, entity != null, "Must create test entity " + type);
+ if (entity instanceof ServerPlayer player) {
+ var connection = new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);
+ new io.netty.channel.embedded.EmbeddedChannel(connection);
+ h.getLevel().getServer().getPlayerList().placeNewPlayer(connection, player,
+ net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(), false));
+ }
+ if (entity instanceof Mob mob) mob.setNoAi(true);
+ entity.setNoGravity(true);
+ entity.setPos(h.absoluteVec(new Vec3(3.5, 1.5, 3.5)));
+ // Initialize actual fluid contact and eye immersion without adding mobs to the world.
+ entity.baseTick();
+ check(h, entity.isEyeInFluid(FluidTags.WATER), "Test entity must be submerged: " + type);
+ return entity;
+ }
+
+ static void run(GameTestHelper h) {
+ for (int x = 1; x <= 6; x++) for (int y = 1; y <= 4; y++) for (int z = 1; z <= 6; z++) {
+ h.setBlock(x, y, z, Blocks.WATER);
+ }
+ var registry = h.getLevel().registryAccess().lookupOrThrow(Registries.ENTITY_TYPE);
+ var protectedTypes = registry.getOrThrow(ModEntityTypeTags.WATER_BREATHING);
+ check(h, protectedTypes.size() >= 25, "All 25 distinct requested vanilla water-breathing types must load");
+ for (var holder : protectedTypes) {
+ var entity = submerged(h, holder.value());
+ check(h, entity.is(EntityTypeTags.CAN_BREATHE_UNDER_WATER), "Vanilla breathing tag must include " + holder);
+ entity.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200, 3));
+ entity.setAirSupply(-20);
+ float health = entity.getHealth();
+ CommonHooks.onLivingBreathe(entity, h.getLevel(), 1, 4);
+ check(h, entity.getAirSupply() == entity.getMaxAirSupply() && entity.getHealth() == health,
+ "Protected type must recover empty air and resist Choke without drowning: " + holder);
+ entity.addEffect(new MobEffectInstance(ModEffects.SINK, 200));
+ check(h, !FluidEffects.cannotRise(entity), "Unlisted water-breather must not be affected by Sink: " + holder);
+ }
+
+ var affectedTypes = registry.getOrThrow(ModEntityTypeTags.SINK_AFFECTED);
+ assertWhitelist(h, ModEntityTypeTags.SINK_AFFECTED, """
+ player fox camel camel_husk chicken mooshroom cow donkey horse mule parrot pig rabbit sheep
+ sniffer tadpole turtle wandering_trader goat villager axolotl cat frog ocelot snow_golem bee
+ panda iron_golem llama polar_bear trader_llama wolf evoker hoglin piglin_brute pillager
+ ravager vindicator warden witch piglin
+ """);
+ assertWhitelist(h, ModEntityTypeTags.CHOKE_AFFECTED, """
+ player fox camel camel_husk chicken mooshroom cow donkey horse mule parrot pig rabbit sheep
+ sniffer turtle wandering_trader goat villager cat frog ocelot snow_golem bee panda iron_golem
+ llama polar_bear trader_llama wolf evoker hoglin piglin_brute pillager ravager vindicator
+ warden witch creeper cave_spider spider enderman piglin
+ """);
+ for (var holder : affectedTypes) {
+ var entity = submerged(h, holder.value());
+ entity.addEffect(new MobEffectInstance(ModEffects.SINK, 200));
+ double y = entity.getY();
+ entity.move(MoverType.SELF, new Vec3(0, 0.1, 0));
+ check(h, entity.getY() == y, "Sink must prevent actual upward motion for " + holder);
+ entity.removeEffect(ModEffects.SINK);
+ entity.move(MoverType.SELF, new Vec3(0, 0.1, 0));
+ check(h, entity.getY() > y, "Removing Sink must restore upward motion for " + holder);
+ }
+
+ for (var type : List.of(EntityTypes.BAT, EntityTypes.CREEPER, EntityTypes.SPIDER,
+ EntityTypes.PHANTOM, EntityTypes.COD)) {
+ var entity = submerged(h, type);
+ entity.addEffect(new MobEffectInstance(ModEffects.SINK, 200));
+ double y = entity.getY();
+ entity.move(MoverType.SELF, new Vec3(0, 0.1, 0));
+ check(h, entity.getY() > y, "Unlisted type/subclass must retain upward movement: " + type);
+ }
+
+ for (var holder : registry.getOrThrow(ModEntityTypeTags.CHOKE_AFFECTED)) {
+ var type = holder.value();
+ var entity = submerged(h, type);
+ entity.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200));
+ entity.setAirSupply(200);
+ entity.baseTick();
+ check(h, entity.getAirSupply() == 198, "Choke I must double actual underwater air loss: " + type);
+ entity.setAirSupply(200);
+ CommonHooks.onLivingBreathe(entity, h.getLevel(), 0, 4);
+ check(h, entity.getAirSupply() == 200, "Choke must preserve Respiration's skipped consumption: " + type);
+ entity.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200, 1));
+ entity.baseTick();
+ check(h, entity.getAirSupply() == 197, "Choke II must triple underwater air loss: " + type);
+ entity.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 200));
+ entity.setAirSupply(200);
+ entity.baseTick();
+ check(h, entity.getAirSupply() >= 200, "Water Breathing must protect against Choke: " + type);
+ entity.removeEffect(MobEffects.WATER_BREATHING);
+ entity.setPos(h.absoluteVec(new Vec3(3.5, 6, 3.5)));
+ entity.baseTick();
+ entity.setAirSupply(200);
+ entity.baseTick();
+ check(h, entity.getAirSupply() == 204, "Ordinary Choke targets must recover air on land: " + type);
+ }
+ for (var type : List.of(EntityTypes.BAT, EntityTypes.ALLAY)) {
+ var entity = submerged(h, type);
+ entity.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200, 2));
+ entity.setAirSupply(200);
+ entity.baseTick();
+ check(h, entity.getAirSupply() == 199, "Unlisted types must retain normal air consumption: " + type);
+ }
+ var phantom = submerged(h, EntityTypes.PHANTOM);
+ phantom.addEffect(new MobEffectInstance(ModEffects.SINK, 200));
+ phantom.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200));
+ phantom.setAirSupply(0);
+ CommonHooks.onLivingBreathe(phantom, h.getLevel(), 1, 4);
+ check(h, !FluidEffects.cannotRise(phantom) && phantom.getAirSupply() > 0,
+ "Phantom must now resist both effects");
+ var player = (ServerPlayer) submerged(h, EntityTypes.PLAYER);
+ // The mock fixes gameMode() to its constructor value; set the abilities used by the breathing hook.
+ GameType.CREATIVE.updatePlayerAbilities(player.getAbilities());
+ player.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200));
+ player.setAirSupply(200);
+ player.baseTick();
+ check(h, player.getAirSupply() >= 200, "Creative invulnerability must protect against Choke: air="
+ + player.getAirSupply() + ", invulnerable=" + player.getAbilities().invulnerable);
+ h.succeed();
+ }
+
+ private static void assertWhitelist(GameTestHelper h, TagKey> tag, String vanillaNames) {
+ Set expected = new HashSet<>();
+ for (String name : vanillaNames.strip().split("\\s+")) expected.add("minecraft:" + name);
+ expected.add("wild_wind_wilderness:muddy_pig");
+ Set actual = new HashSet<>();
+ for (var holder : h.getLevel().registryAccess().lookupOrThrow(Registries.ENTITY_TYPE).getOrThrow(tag)) {
+ actual.add(BuiltInRegistries.ENTITY_TYPE.getKey(holder.value()).toString());
+ }
+ check(h, actual.equals(expected), "Exact whitelist must match " + tag + ": " + actual);
+ }
+
+ static void landChoke(GameTestHelper h) {
+ for (int x = 1; x <= 6; x++) for (int y = 1; y <= 4; y++) for (int z = 1; z <= 6; z++) {
+ h.setBlock(x, y, z, Blocks.WATER);
+ }
+ // Keep the dry position sheltered so rain cannot rehydrate the axolotl.
+ h.setBlock(3, 8, 3, Blocks.STONE);
+ for (var type : List.of(EntityTypes.TADPOLE, EntityTypes.AXOLOTL)) {
+ var entity = submerged(h, type);
+ ((Mob) entity).setNoAi(false);
+ check(h, !entity.is(ModEntityTypeTags.CHOKE_AFFECTED), "Land-only types must not enter the water Choke tag");
+ entity.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200));
+ entity.setAirSupply(200);
+ entity.baseTick();
+ check(h, entity.getAirSupply() == entity.getMaxAirSupply(), "Choke must not affect submerged " + type);
+ entity.setPos(h.absoluteVec(new Vec3(3.5, 6, 3.5)));
+ entity.baseTick();
+ check(h, !entity.isInWater(), "Special Choke test must be on dry land");
+ entity.setAirSupply(200);
+ entity.baseTick();
+ check(h, entity.getAirSupply() == 198, "Choke I must double real land drying: " + type);
+ entity.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200, 1));
+ entity.setAirSupply(200);
+ entity.baseTick();
+ check(h, entity.getAirSupply() == 197, "Choke II must triple real land drying: " + type);
+ entity.removeEffect(ModEffects.CHOKE);
+ entity.setAirSupply(200);
+ entity.baseTick();
+ check(h, entity.getAirSupply() == 199, "Removing Choke must restore vanilla drying: " + type);
+ entity.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200));
+ entity.setAirSupply(-18);
+ entity.invulnerableTime = 0;
+ float health = entity.getHealth();
+ entity.baseTick();
+ check(h, entity.getHealth() == health - 2 && entity.getAirSupply() == 0,
+ "Land Choke must use vanilla drying damage exactly once: " + type
+ + ", health " + health + " -> " + entity.getHealth() + ", air=" + entity.getAirSupply());
+ entity.setPos(h.absoluteVec(new Vec3(3.5, 1.5, 3.5)));
+ entity.baseTick();
+ check(h, entity.getAirSupply() == entity.getMaxAirSupply(), "Returning to water must stop Choke and refill air: " + type);
+ }
+ var cod = submerged(h, EntityTypes.COD);
+ cod.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200, 2));
+ cod.setPos(h.absoluteVec(new Vec3(3.5, 6, 3.5)));
+ cod.baseTick();
+ cod.setAirSupply(200);
+ cod.baseTick();
+ check(h, cod.getAirSupply() == 199, "Shared WaterAnimal mixin must not accelerate other fish drying");
+ h.succeed();
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/hfas/test/GameplayGameTests.java b/src/verification/java/org/polaris2023/wildwind/hfas/test/GameplayGameTests.java
new file mode 100644
index 0000000..ba2bc64
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/hfas/test/GameplayGameTests.java
@@ -0,0 +1,274 @@
+package org.polaris2023.wildwind.hfas.test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.tags.ItemTags;
+import net.minecraft.world.effect.MobEffectInstance;
+import net.minecraft.world.effect.MobEffects;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.entity.MoverType;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.inventory.ContainerLevelAccess;
+import net.minecraft.world.inventory.CraftingMenu;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.crafting.CraftingInput;
+import net.minecraft.world.item.crafting.RecipeType;
+import net.minecraft.world.item.enchantment.Enchantments;
+import net.minecraft.world.level.GameType;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.entity.CrafterBlockEntity;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.Vec3;
+import net.neoforged.bus.api.IEventBus;
+import net.neoforged.neoforge.common.CommonHooks;
+import net.neoforged.neoforge.common.NeoForge;
+import net.neoforged.neoforge.event.level.BlockDropsEvent;
+import net.neoforged.neoforge.registries.DeferredRegister;
+import org.polaris2023.wildwind.wilderness.effect.ModEffects;
+import org.polaris2023.wildwind.core.enchantment.MeltingEnchantment;
+import org.polaris2023.wildwind.core.event.CobwebBreaking;
+import org.polaris2023.wildwind.core.recipe.WoolToStringRecipe;
+
+public final class GameplayGameTests {
+ private GameplayGameTests() {}
+ public static void register(IEventBus bus) {
+ DeferredRegister> tests = DeferredRegister.create(Registries.TEST_FUNCTION, "wild_wind_tests");
+ tests.register("wool_recipe", () -> GameplayGameTests::woolRecipe);
+ tests.register("crafter_shears", () -> GameplayGameTests::crafterShears);
+ tests.register("fluid_effects", () -> GameplayGameTests::fluidEffects);
+ tests.register("fluid_entity_rules", () -> FluidEntityRulesGameTests::run);
+ tests.register("land_choke", () -> FluidEntityRulesGameTests::landChoke);
+ tests.register("azalea_signs", () -> AzaleaAndArrowGameTests::signs);
+ tests.register("azalea_shelf", () -> AzaleaAndArrowGameTests::shelf);
+ tests.register("azalea_shelf_chain", () -> AzaleaAndArrowGameTests::shelfChain);
+ tests.register("reach_arrow_acquisition", () -> AzaleaAndArrowGameTests::acquisition);
+ tests.register("reach_arrows", () -> AzaleaAndArrowGameTests::arrows);
+ tests.register("kiln", () -> KilnGameTests::cooking);
+ tests.register("kiln_inventory", () -> KilnGameTests::inventory);
+ tests.register("kiln_villages", () -> KilnGameTests::villages);
+ tests.register("upstream_integration", () -> UpstreamIntegrationGameTests::run);
+ tests.register("cobweb_timers", () -> GameplayGameTests::cobwebTimers);
+ tests.register("cobweb_tick_event", () -> GameplayGameTests::cobwebTickEvent);
+ tests.register("melting", () -> GameplayGameTests::melting);
+ tests.register("melting_table", () -> EnchantmentTableGameTests::melting);
+ tests.register("skeleton_spawns", () -> SkeletonAndFarmlandGameTests::skeletonSpawns);
+ tests.register("skeleton_melee", () -> SkeletonAndFarmlandGameTests::skeletonMelee);
+ tests.register("farmland_protection", () -> SkeletonAndFarmlandGameTests::farmland);
+ tests.register("creative_tab", () -> SkeletonAndFarmlandGameTests::creativeTab);
+ tests.register("inspection_blocks", () -> BookInspectionGameTests::blocks);
+ tests.register("inspection_interactions", () -> BookInspectionGameTests::interactions);
+ tests.register("inspection_sulfur", () -> BookInspectionGameTests::sulfur);
+ tests.register(bus);
+ }
+ private static void check(GameTestHelper h, boolean ok, String message) {
+ h.assertTrue(ok, Component.literal(message));
+ }
+
+ private static void woolRecipe(GameTestHelper h) {
+ var level = h.getLevel();
+ var recipe = WoolToStringRecipe.INSTANCE;
+ var shears = new ItemStack(Items.SHEARS);
+ shears.set(DataComponents.CUSTOM_NAME, Component.literal("Test shears"));
+ for (var wool : level.registryAccess().lookupOrThrow(Registries.ITEM).getOrThrow(ItemTags.WOOL)) {
+ for (int woolSlot = 0; woolSlot < 4; woolSlot++) {
+ var stacks = new ArrayList<>(List.of(ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY));
+ stacks.set(woolSlot, new ItemStack(wool));
+ stacks.set((woolSlot + 1) % 4, shears.copy());
+ var input = CraftingInput.of(2, 2, stacks);
+ var found = level.recipeAccess().getRecipeFor(RecipeType.CRAFTING, input, level);
+ check(h, found.isPresent() && found.get().value() instanceof WoolToStringRecipe, "All wool colors and slot orders must match registered recipe");
+ check(h, recipe.assemble(input).is(Items.STRING) && recipe.assemble(input).getCount() == 2, "One wool must give two string");
+ var remainder = recipe.remainingItems(input, level).stream().filter(s -> !s.isEmpty()).findFirst().orElseThrow();
+ check(h, remainder.getDamageValue() == 1 && remainder.getHoverName().getString().equals("Test shears"), "Shears must retain data and take one damage");
+ check(h, shears.getDamageValue() == 0, "Crafting must not mutate ingredient during preview");
+ }
+ }
+ check(h, !recipe.matches(CraftingInput.of(2, 2, List.of(new ItemStack(Items.WOOL.white()), shears, new ItemStack(Items.DIRT), ItemStack.EMPTY)), level), "Extra ingredients must fail");
+ check(h, !recipe.matches(CraftingInput.of(2, 1, List.of(new ItemStack(Items.WOOL.white()), new ItemStack(Items.STRING))), level), "Wrong tool must fail");
+ var broken = shears.copy(); broken.setDamageValue(broken.getMaxDamage() - 1);
+ check(h, recipe.remainingItems(CraftingInput.of(2, 1, List.of(new ItemStack(Items.WOOL.white()), broken)), level).stream().allMatch(ItemStack::isEmpty), "Last durability must break the shears");
+ var enchanted = shears.copy();
+ var unbreaking = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.UNBREAKING);
+ enchanted.enchant(unbreaking, 3);
+ int damage = 0;
+ for (int i = 0; i < 256; i++) {
+ var remainder = recipe.remainingItems(CraftingInput.of(2, 1, List.of(new ItemStack(Items.WOOL.white()), enchanted)), level).get(1);
+ check(h, remainder.getEnchantments().getLevel(unbreaking) == 3, "Unbreaking must survive crafting");
+ damage += remainder.getDamageValue();
+ }
+ check(h, damage > 0 && damage < 160, "Unbreaking III must probabilistically reduce crafting damage");
+ var player = (ServerPlayer) h.makeMockServerPlayer(GameType.SURVIVAL);
+ var connection = new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);
+ new io.netty.channel.embedded.EmbeddedChannel(connection);
+ level.getServer().getPlayerList().placeNewPlayer(connection, player,
+ net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(), false));
+ var menu = new CraftingMenu(0, player.getInventory(), ContainerLevelAccess.create(level, h.absolutePos(new BlockPos(2, 1, 2))));
+ menu.getSlot(1).set(new ItemStack(Items.WOOL.white(), 3));
+ menu.getSlot(2).set(shears.copy());
+ // Invoke the real result-slot path twice, including CommonHooks crafting-player context.
+ for (int i = 0; i < 2; i++) {
+ var result = menu.getSlot(0).remove(2);
+ check(h, result.is(Items.STRING) && result.getCount() == 2, "Crafting menu must offer the registered output");
+ menu.getSlot(0).onTake(player, result);
+ }
+ check(h, menu.getSlot(1).getItem().getCount() == 1 && menu.getSlot(2).getItem().getDamageValue() == 2, "Repeated crafts must consume one wool and one durability each");
+ menu.getSlot(1).set(new ItemStack(Items.WOOL.white(), 3));
+ for (int i = 0; i < 3; i++) menu.quickMoveStack(player, 0);
+ check(h, menu.getSlot(1).getItem().isEmpty() && menu.getSlot(2).getItem().getDamageValue() == 5,
+ "Shift-click crafts must return shears and charge durability for every wool");
+ check(h, player.getInventory().countItem(Items.STRING) == 6, "Shift-click must transfer all six string");
+ h.succeed();
+ }
+
+ private static void crafterShears(GameTestHelper h) {
+ var pos = new BlockPos(3, 2, 3);
+ h.setBlock(pos, Blocks.CRAFTER);
+ var crafter = (CrafterBlockEntity) h.getLevel().getBlockEntity(h.absolutePos(pos));
+ crafter.setItem(0, new ItemStack(Items.WOOL.white()));
+ crafter.setItem(1, new ItemStack(Items.SHEARS));
+ h.setBlock(pos.below(), Blocks.REDSTONE_BLOCK);
+ h.runAfterDelay(10, () -> {
+ var drops = h.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(h.absolutePos(pos)).inflate(4));
+ check(h, drops.stream().anyMatch(e -> e.getItem().is(Items.STRING) && e.getItem().getCount() == 2), "Crafter must eject two string");
+ check(h, drops.stream().anyMatch(e -> e.getItem().is(Items.SHEARS) && e.getItem().getDamageValue() == 1), "Crafter must eject damaged shears using server durability rules");
+ h.succeed();
+ });
+ }
+
+ private static void fluidEffects(GameTestHelper h) {
+ for (int x = 1; x <= 3; x++) for (int y = 1; y <= 4; y++) for (int z = 1; z <= 3; z++) h.setBlock(x, y, z, Blocks.WATER);
+ for (int x = 5; x <= 6; x++) for (int y = 1; y <= 4; y++) for (int z = 1; z <= 3; z++) h.setBlock(x, y, z, Blocks.LAVA);
+ var pig = h.spawn(EntityTypes.PIG, 2, 2, 2); pig.setNoAi(true); pig.setNoGravity(true);
+ var lavaPig = h.spawn(EntityTypes.PIG, 5, 2, 2); lavaPig.setNoAi(true); lavaPig.setNoGravity(true); lavaPig.setInvulnerable(true);
+ pig.addEffect(new MobEffectInstance(ModEffects.SINK, 200));
+ pig.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200));
+ lavaPig.addEffect(new MobEffectInstance(ModEffects.SINK, 200));
+ h.runAfterDelay(3, () -> {
+ double y = pig.getY(); pig.move(MoverType.SELF, new Vec3(0, 0.25, 0));
+ check(h, pig.getY() == y, "Sink must stop actual upward water movement");
+ double lavaY = lavaPig.getY(); lavaPig.move(MoverType.SELF, new Vec3(0, 0.25, 0));
+ check(h, lavaPig.getY() == lavaY, "Sink must also stop lava buoyancy");
+ pig.move(MoverType.SELF, new Vec3(0.05, -0.05, 0));
+ check(h, pig.getY() < y, "Sink must allow downward and horizontal motion");
+ pig.setAirSupply(200); CommonHooks.onLivingBreathe(pig, h.getLevel(), 1, 4);
+ check(h, pig.getAirSupply() == 198, "Choke I must consume two air per normal breathing tick");
+ pig.addEffect(new MobEffectInstance(ModEffects.CHOKE, 200, 1));
+ pig.setAirSupply(200); CommonHooks.onLivingBreathe(pig, h.getLevel(), 1, 4);
+ check(h, pig.getAirSupply() == 197, "Choke II must consume three air");
+ pig.setAirSupply(200); CommonHooks.onLivingBreathe(pig, h.getLevel(), 0, 4);
+ check(h, pig.getAirSupply() == 200, "Respiration's skipped consumption must remain skipped");
+ pig.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 200));
+ CommonHooks.onLivingBreathe(pig, h.getLevel(), 1, 4);
+ check(h, pig.getAirSupply() == 200, "Water breathing must continue to protect oxygen");
+ pig.removeEffect(ModEffects.SINK); double before = pig.getY(); pig.move(MoverType.SELF, new Vec3(0, 0.1, 0));
+ check(h, pig.getY() > before, "Removing Sink must restore upward movement");
+ h.succeed();
+ });
+ }
+
+ private static void moveWeb(Entity entity, int ticks, boolean falling) {
+ for (int i = 0; i < ticks; i++) {
+ entity.setPos(entity.getX() + (falling ? 0 : (i % 2 == 0 ? 0.001 : -0.001)), entity.getY() - (falling ? 0.001 : 0), entity.getZ());
+ CobwebBreaking.tick(entity);
+ }
+ }
+ private static void cobwebTimers(GameTestHelper h) {
+ var pos = new BlockPos(3, 2, 3);
+ h.setBlock(pos, Blocks.COBWEB);
+ var pig = h.spawn(EntityTypes.PIG, 3.5F, 2.3F, 3.5F); pig.setNoAi(true); pig.setNoGravity(true); pig.setOnGround(true);
+ CobwebBreaking.tick(pig); moveWeb(pig, 29, false);
+ h.assertBlockPresent(Blocks.COBWEB, pos);
+ CobwebBreaking.tick(pig); // No displacement resets the counter.
+ moveWeb(pig, 29, false); h.assertBlockPresent(Blocks.COBWEB, pos);
+ moveWeb(pig, 1, false); h.assertBlockPresent(Blocks.AIR, pos);
+ h.setBlock(pos, Blocks.COBWEB); pig.setOnGround(false);
+ moveWeb(pig, 159, true); h.assertBlockPresent(Blocks.COBWEB, pos);
+ moveWeb(pig, 1, true); h.assertBlockPresent(Blocks.AIR, pos);
+ h.setBlock(pos, Blocks.COBWEB);
+ var spider = h.spawn(EntityTypes.SPIDER, 3.5F, 2.3F, 3.5F);
+ var caveSpider = h.spawn(EntityTypes.CAVE_SPIDER, 3.5F, 2.3F, 3.5F);
+ for (var entity : List.of(spider, caveSpider)) { CobwebBreaking.tick(entity); moveWeb(entity, 200, false); }
+ h.assertBlockPresent(Blocks.COBWEB, pos);
+ for (var mode : List.of(GameType.SPECTATOR, GameType.CREATIVE)) {
+ var player = h.makeMockPlayer(mode); player.getAbilities().flying = true;
+ player.setPos(pig.position()); CobwebBreaking.tick(player); moveWeb(player, 200, false);
+ h.assertBlockPresent(Blocks.COBWEB, pos);
+ }
+ // Counter belongs to the touched block, not a global web timer.
+ pig.setOnGround(true); CobwebBreaking.tick(pig); moveWeb(pig, 20, false);
+ pig.setPos(h.absoluteVec(new Vec3(6.5, 2.3, 3.5))); CobwebBreaking.tick(pig);
+ pig.setPos(h.absoluteVec(new Vec3(3.5, 2.3, 3.5))); CobwebBreaking.tick(pig);
+ moveWeb(pig, 29, false); h.assertBlockPresent(Blocks.COBWEB, pos);
+ moveWeb(pig, 1, false); h.assertBlockPresent(Blocks.AIR, pos);
+ h.succeed();
+ }
+
+ private static void cobwebTickEvent(GameTestHelper h) {
+ var pos = new BlockPos(3, 2, 3); h.setBlock(pos, Blocks.COBWEB);
+ var pig = h.spawn(EntityTypes.PIG, 3.5F, 2.3F, 3.5F); pig.setNoAi(true); pig.setNoGravity(true);
+ // Actual entity tick events must drive the timer, even with no explicit calls to the handler.
+ for (int i = 1; i <= 35; i++) {
+ final int tick = i;
+ h.runAfterDelay(i, () -> pig.setPos(pig.getX() + (tick % 2 == 0 ? 0.001 : -0.001), pig.getY(), pig.getZ()));
+ }
+ h.runAfterDelay(37, () -> { h.assertBlockPresent(Blocks.AIR, pos); h.succeed(); });
+ }
+
+ private static BlockDropsEvent drops(GameTestHelper h, ServerPlayer player, ItemStack tool, List stacks) {
+ var entities = new ArrayList();
+ for (var stack : stacks) entities.add(new ItemEntity(h.getLevel(), 0, 0, 0, stack));
+ var event = new BlockDropsEvent(h.getLevel(), h.absolutePos(new BlockPos(3, 1, 3)), Blocks.IRON_ORE.defaultBlockState(), null, entities, player, tool);
+ NeoForge.EVENT_BUS.post(event);
+ return event;
+ }
+ private static int count(BlockDropsEvent event, net.minecraft.world.item.Item item) {
+ return event.getDrops().stream().map(ItemEntity::getItem).filter(s -> s.is(item)).mapToInt(ItemStack::getCount).sum();
+ }
+ private static void melting(GameTestHelper h) {
+ var level = h.getLevel();
+ var player = (ServerPlayer) h.makeMockServerPlayer(GameType.SURVIVAL);
+ var registry = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT);
+ var tool = new ItemStack(Items.DIAMOND_PICKAXE); tool.enchant(registry.getOrThrow(MeltingEnchantment.MELTING), 1);
+ player.setExperienceLevels(0); player.setExperiencePoints(2); player.totalExperience = 9999;
+ var event = drops(h, player, tool, List.of(new ItemStack(Items.RAW_IRON, 3)));
+ check(h, count(event, Items.RAW_IRON) == 3 && MeltingEnchantment.availableExperience(player) == 2, "Insufficient current XP must preserve entire drop; lifetime total is not fuel");
+ player.setExperiencePoints(5);
+ event = drops(h, player, tool, List.of(new ItemStack(Items.RAW_IRON, 3), new ItemStack(Items.DIAMOND, 2)));
+ check(h, count(event, Items.IRON_INGOT) == 3 && count(event, Items.DIAMOND) == 2 && MeltingEnchantment.availableExperience(player) == 2, "Melt only smeltable drops and charge one point per input");
+ var silk = tool.copy(); silk.enchant(registry.getOrThrow(Enchantments.SILK_TOUCH), 1);
+ event = drops(h, player, silk, List.of(new ItemStack(Items.IRON_ORE)));
+ check(h, count(event, Items.IRON_ORE) == 1 && MeltingEnchantment.availableExperience(player) == 2, "Silk Touch must take precedence without XP use");
+ player.setExperienceLevels(1); player.setExperiencePoints(0);
+ event = drops(h, player, tool, List.of(new ItemStack(Items.RAW_IRON)));
+ check(h, count(event, Items.IRON_INGOT) == 1 && player.experienceLevel == 0 && MeltingEnchantment.availableExperience(player) == 6, "Spend exactly one point across level boundary");
+ var fortune = tool.copy(); fortune.enchant(registry.getOrThrow(Enchantments.FORTUNE), 3);
+ player.setExperienceLevels(10); player.setExperiencePoints(0);
+ var loot = Block.getDrops(Blocks.IRON_ORE.defaultBlockState(), level, h.absolutePos(new BlockPos(3, 1, 3)), null, player, fortune);
+ int inputCount = loot.stream().filter(s -> s.is(Items.RAW_IRON)).mapToInt(ItemStack::getCount).sum();
+ long before = MeltingEnchantment.availableExperience(player);
+ event = drops(h, player, fortune, loot);
+ check(h, inputCount > 0 && count(event, Items.IRON_INGOT) == inputCount && MeltingEnchantment.availableExperience(player) == before - inputCount, "Fortune loot must be determined first and fully charged");
+ for (int currentLevel : new int[]{16, 17, 30, 31, 32}) {
+ player.setExperienceLevels(currentLevel); player.setExperiencePoints(0); before = MeltingEnchantment.availableExperience(player);
+ event = drops(h, player, tool, List.of(new ItemStack(Items.RAW_IRON)));
+ check(h, count(event, Items.IRON_INGOT) == 1 && MeltingEnchantment.availableExperience(player) == before - 1, "XP formula at level " + currentLevel);
+ }
+ // Exercise vanilla block dropping, rather than only posting the event directly.
+ var pos = h.absolutePos(new BlockPos(5, 2, 5)); before = MeltingEnchantment.availableExperience(player);
+ Block.dropResources(Blocks.IRON_ORE.defaultBlockState(), level, pos, null, player, tool);
+ check(h, MeltingEnchantment.availableExperience(player) == before - 1
+ && level.getEntitiesOfClass(ItemEntity.class, new AABB(pos).inflate(1)).stream().anyMatch(e -> e.getItem().is(Items.IRON_INGOT)),
+ "Vanilla block loot path must invoke Melting");
+ h.succeed();
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/hfas/test/KilnGameTests.java b/src/verification/java/org/polaris2023/wildwind/hfas/test/KilnGameTests.java
new file mode 100644
index 0000000..d0abbf2
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/hfas/test/KilnGameTests.java
@@ -0,0 +1,230 @@
+package org.polaris2023.wildwind.hfas.test;
+
+import java.util.List;
+import java.util.Arrays;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.advancements.predicates.ItemPredicate;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.LockCode;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.entity.ExperienceOrb;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.level.GameType;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.HopperBlock;
+import net.minecraft.world.level.block.Rotation;
+import net.minecraft.world.level.block.entity.HopperBlockEntity;
+import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
+import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
+import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
+import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.Vec3;
+import org.polaris2023.wildwind.core.worldgen.MasonKilnProcessor;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.level.block.AbstractFurnaceBlock;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import org.polaris2023.wildwind.core.block.entity.KilnBlockEntity;
+import org.polaris2023.wildwind.core.recipe.KilnRecipes;
+
+final class KilnGameTests {
+ private KilnGameTests() {}
+
+ private static void check(GameTestHelper h, boolean ok, String message) {
+ h.assertTrue(ok, Component.literal(message));
+ }
+
+ static void cooking(GameTestHelper h) {
+ var pos = h.absolutePos(new net.minecraft.core.BlockPos(2, 1, 2));
+ h.setBlock(2, 1, 2, org.polaris2023.wildwind.core.block.ModBlocks.KILN.get());
+ BlockEntity raw = h.getLevel().getBlockEntity(pos);
+ check(h, raw instanceof KilnBlockEntity, "Kiln must create its block entity");
+ var kiln = (KilnBlockEntity) raw;
+ var state = kiln.getBlockState();
+ check(h, state.getDestroySpeed(h.getLevel(), pos) == 3.5F && org.polaris2023.wildwind.core.block.ModBlocks.KILN.get().getExplosionResistance() == 3.5F,
+ "Kiln hardness and blast resistance must both be 3.5");
+ check(h, state.getPistonPushReaction() == net.minecraft.world.level.material.PushReaction.BLOCK
+ && state.isRedstoneConductor(h.getLevel(), pos) && state.isSuffocating(h.getLevel(), pos),
+ "Kiln must be a solid conductor that pistons cannot move");
+ var recipeInput = net.minecraft.world.item.crafting.CraftingInput.of(3, 3, List.of(
+ ItemStack.EMPTY, new ItemStack(Items.BRICK), ItemStack.EMPTY,
+ new ItemStack(Items.BRICK), new ItemStack(Items.FURNACE), new ItemStack(Items.BRICK),
+ new ItemStack(Items.PACKED_MUD), new ItemStack(Items.PACKED_MUD), new ItemStack(Items.PACKED_MUD)));
+ check(h, h.getLevel().recipeAccess().getRecipeFor(net.minecraft.world.item.crafting.RecipeType.CRAFTING, recipeInput, h.getLevel())
+ .orElseThrow().value().assemble(recipeInput).is(org.polaris2023.wildwind.core.block.ModBlocks.KILN.get().asItem()), "Requested kiln crafting pattern must work");
+ kiln.setItem(0, new ItemStack(Items.CACTUS, 2));
+ kiln.setItem(1, new ItemStack(Items.COAL));
+ check(h, KilnRecipes.find(h.getLevel(), kiln.getItem(0)).isPresent(), "Cactus must be kiln-smeltable");
+ check(h, KilnRecipes.find(h.getLevel(), new ItemStack(Items.IRON_ORE)).isEmpty(), "Ore must be excluded");
+ for (var denied : List.of(Items.RAW_IRON, Items.IRON_PICKAXE, Items.GOLDEN_CHESTPLATE, Items.ANCIENT_DEBRIS)) {
+ check(h, KilnRecipes.find(h.getLevel(), new ItemStack(denied)).isEmpty(), "Metal/mineral must be excluded: " + denied);
+ check(h, !kiln.canPlaceItem(0, new ItemStack(denied)), "Automated input must reject excluded materials");
+ }
+ for (var allowed : List.of(Items.SAND, Items.CLAY_BALL, Items.COBBLESTONE, Items.OAK_LOG, Items.WET_SPONGE, Items.POTATO)) {
+ check(h, KilnRecipes.find(h.getLevel(), new ItemStack(allowed)).isPresent(), "Non-mineral smelting recipe must work: " + allowed);
+ }
+ tick(h, kiln, 99);
+ check(h, kiln.getItem(2).isEmpty(), "Nothing may finish before tick 100");
+ var ticker = org.polaris2023.wildwind.core.block.ModBlocks.KILN.get().getTicker(h.getLevel(), state,
+ org.polaris2023.wildwind.core.block.entity.ModBlockEntities.KILN.get());
+ check(h, ticker != null, "Kiln must have a registered server ticker");
+ ticker.tick(h.getLevel(), pos, h.getLevel().getBlockState(pos), kiln);
+ check(h, kiln.getItem(0).getCount() == 1, "Kiln must consume exactly one input after 100 ticks");
+ check(h, kiln.getItem(2).is(Items.DYE.green()) && kiln.getItem(2).getCount() == 1,
+ "Kiln must produce one cactus green dye");
+ check(h, kiln.getItem(1).isEmpty(), "One coal must be consumed when the kiln starts");
+ check(h, h.getLevel().getBlockState(pos).getValue(AbstractFurnaceBlock.LIT),
+ "Kiln must be lit while its fuel remains");
+ check(h, h.getLevel().getBlockState(pos).getLightEmission() == 13, "Burning kiln must emit light 13");
+ var saved = kiln.saveWithFullMetadata(h.getLevel().registryAccess());
+ var loaded = (KilnBlockEntity) BlockEntity.loadStatic(pos, kiln.getBlockState(), saved, h.getLevel().registryAccess());
+ check(h, loaded != null && loaded.getItem(2).is(Items.DYE.green()), "Contents must survive reload");
+ loaded.setLevel(h.getLevel());
+ tick(h, loaded, 100);
+ check(h, loaded.getItem(2).getCount() == 2 && loaded.getItem(0).isEmpty(), "Fuel/progress must continue after reload");
+ kiln.setItem(0, new ItemStack(Items.CACTUS));
+ kiln.setItem(2, new ItemStack(Items.DYE.green(), 64));
+ tick(h, kiln, 100);
+ check(h, kiln.getItem(0).getCount() == 1 && kiln.getItem(2).getCount() == 64, "Full output must prevent consumption");
+ kiln.setItem(2, new ItemStack(Items.STONE));
+ tick(h, kiln, 100);
+ check(h, kiln.getItem(0).getCount() == 1 && kiln.getItem(2).is(Items.STONE), "Different output must prevent cooking");
+ kiln.setItem(0, new ItemStack(Items.IRON_ORE));
+ kiln.setItem(2, ItemStack.EMPTY);
+ tick(h, kiln, 100);
+ check(h, kiln.getItem(2).isEmpty() && kiln.getItem(0).is(Items.IRON_ORE), "Forced excluded input must not cook");
+ kiln.setItem(0, new ItemStack(Items.WET_SPONGE));
+ kiln.setItem(1, new ItemStack(Items.BUCKET));
+ tick(h, kiln, 100);
+ check(h, kiln.getItem(2).is(Items.SPONGE) && kiln.getItem(1).is(Items.WATER_BUCKET), "Wet sponge must fill the fuel-slot bucket");
+ var longFuel = kiln.saveWithFullMetadata(h.getLevel().registryAccess());
+ longFuel.putInt("lit_time_remaining", 50000);
+ longFuel.putInt("lit_total_time", 50000);
+ var longBurn = (KilnBlockEntity) BlockEntity.loadStatic(pos, kiln.getBlockState(), longFuel, h.getLevel().registryAccess());
+ longBurn.setLevel(h.getLevel());
+ tick(h, longBurn, 1);
+ check(h, longBurn.saveCustomOnly(h.getLevel().registryAccess()).getIntOr("lit_time_remaining", 0) == 49999,
+ "Long modded fuel timers must not be truncated by network short scaling");
+ h.succeed();
+ }
+
+ private static void tick(GameTestHelper h, KilnBlockEntity kiln, int ticks) {
+ for (int i = 0; i < ticks; i++) KilnBlockEntity.tick(h.getLevel(), kiln.getBlockPos(), h.getLevel().getBlockState(kiln.getBlockPos()), kiln);
+ }
+
+ static void inventory(GameTestHelper h) {
+ var level = h.getLevel();
+ var pos = h.absolutePos(new BlockPos(3, 2, 3));
+ h.setBlock(3, 2, 3, org.polaris2023.wildwind.core.block.ModBlocks.KILN.get());
+ var kiln = (KilnBlockEntity) level.getBlockEntity(pos);
+ check(h, Arrays.equals(kiln.getSlotsForFace(Direction.UP), new int[]{0})
+ && Arrays.equals(kiln.getSlotsForFace(Direction.DOWN), new int[]{2, 1})
+ && Arrays.equals(kiln.getSlotsForFace(Direction.EAST), new int[]{1}), "Sided slots must match the specification");
+ h.setBlock(3, 3, 3, Blocks.HOPPER);
+ var top = (HopperBlockEntity) level.getBlockEntity(pos.above());
+ top.setItem(0, new ItemStack(Items.CACTUS));
+ HopperBlockEntity.pushItemsTick(level, top.getBlockPos(), top.getBlockState(), top);
+ check(h, kiln.getItem(0).is(Items.CACTUS), "Top hopper must insert ingredients");
+ h.setBlock(4, 2, 3, Blocks.HOPPER.defaultBlockState().setValue(HopperBlock.FACING, Direction.WEST));
+ var side = (HopperBlockEntity) level.getBlockEntity(pos.east());
+ side.setItem(0, new ItemStack(Items.LAVA_BUCKET));
+ HopperBlockEntity.pushItemsTick(level, side.getBlockPos(), side.getBlockState(), side);
+ check(h, kiln.getItem(1).is(Items.LAVA_BUCKET), "Side hopper must insert fuel");
+ tick(h, kiln, 100);
+ check(h, kiln.getItem(1).is(Items.BUCKET), "Lava fuel must return an empty bucket");
+ h.setBlock(3, 1, 3, Blocks.HOPPER);
+ var bottom = (HopperBlockEntity) level.getBlockEntity(pos.below());
+ check(h, HopperBlockEntity.suckInItems(level, bottom), "Bottom hopper must pull output");
+ check(h, bottom.getItem(0).is(Items.DYE.green()) && kiln.getItem(1).is(Items.BUCKET), "Output must be pulled before the bucket");
+ check(h, HopperBlockEntity.suckInItems(level, bottom) && kiln.getItem(1).isEmpty(), "Bottom hopper must next pull the bucket");
+ check(h, kiln.saveCustomOnly(level.registryAccess()).getCompound("RecipesUsed").orElseThrow().size() > 0,
+ "Hopper extraction must retain stored experience");
+ kiln.clearContent();
+ check(h, AbstractContainerMenu.getRedstoneSignalFromBlockEntity(kiln) == 0, "Empty kiln comparator must be zero");
+ kiln.setItem(0, new ItemStack(Items.CACTUS, 64));
+ kiln.setItem(1, new ItemStack(Items.COAL, 64));
+ kiln.setItem(2, new ItemStack(Items.DYE.green(), 64));
+ check(h, kiln.getBlockState().getAnalogOutputSignal(level, pos, Direction.NORTH) == 15, "Comparator must count all three slots");
+ kiln.clearContent();
+ var player = connectedPlayer(h);
+ var named = new ItemStack(org.polaris2023.wildwind.core.block.ModBlocks.KILN.get());
+ named.set(DataComponents.CUSTOM_NAME, Component.literal("Test kiln"));
+ named.set(DataComponents.LOCK, new LockCode(ItemPredicate.Builder.item()
+ .of(level.registryAccess().lookupOrThrow(Registries.ITEM), Items.BRICK).build()));
+ kiln.applyComponentsFromItemStack(named);
+ check(h, kiln.getName().getString().equals("Test kiln") && !kiln.canOpen(player), "Custom name and lock must apply");
+ player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.BRICK));
+ check(h, kiln.canOpen(player), "Matching key item must unlock kiln");
+ var menu = kiln.createMenu(1, player.getInventory(), player);
+ check(h, menu != null && !menu.getSlot(0).mayPlace(new ItemStack(Items.IRON_ORE)), "GUI must reject excluded input");
+ check(h, !menu.getSlot(2).mayPlace(new ItemStack(Items.STONE)), "Output slot must reject insertion");
+ kiln.setItem(0, new ItemStack(Items.CACTUS));
+ tick(h, kiln, 100);
+ var outputSlot = menu.getSlot(2);
+ var result = outputSlot.remove(1);
+ outputSlot.onTake(player, result);
+ check(h, kiln.saveCustomOnly(level.registryAccess()).getCompound("RecipesUsed").orElseThrow().isEmpty(),
+ "Manual extraction must release and clear stored experience");
+ check(h, !level.getEntitiesOfClass(ExperienceOrb.class, new AABB(player.blockPosition()).inflate(2)).isEmpty(), "Manual extraction must spawn stored experience");
+ kiln.setItem(0, new ItemStack(Items.CACTUS));
+ tick(h, kiln, 100);
+ var state = kiln.getBlockState();
+ check(h, Block.getDrops(state, level, pos, kiln, player, new ItemStack(Items.WOODEN_PICKAXE)).stream()
+ .anyMatch(stack -> stack.is(org.polaris2023.wildwind.core.block.ModBlocks.KILN.get().asItem())), "Wooden pickaxe must allow kiln loot");
+ check(h, !player.hasCorrectToolForDrops(state), "A held brick must not qualify for kiln drops");
+ player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.WOODEN_PICKAXE));
+ check(h, player.hasCorrectToolForDrops(state), "Wooden pickaxe must qualify for kiln drops");
+ level.destroyBlock(pos, true, player);
+ var dropped = level.getEntitiesOfClass(ItemEntity.class, new AABB(pos).inflate(2));
+ check(h, dropped.stream().anyMatch(e -> e.getItem().is(org.polaris2023.wildwind.core.block.ModBlocks.KILN.get().asItem()))
+ && dropped.stream().anyMatch(e -> e.getItem().is(Items.DYE.green())), "Breaking must drop the kiln and its inventory");
+ check(h, !level.getEntitiesOfClass(ExperienceOrb.class, new AABB(pos).inflate(2)).isEmpty(), "Breaking must release stored experience");
+ h.succeed();
+ }
+
+ private static ServerPlayer connectedPlayer(GameTestHelper h) {
+ var player = (ServerPlayer) h.makeMockServerPlayer(GameType.SURVIVAL);
+ var connection = new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);
+ new io.netty.channel.embedded.EmbeddedChannel(connection);
+ h.getLevel().getServer().getPlayerList().placeNewPlayer(connection, player,
+ net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(), false));
+ player.setPos(h.absoluteVec(new Vec3(6.5, 2, 6.5)));
+ return player;
+ }
+
+ static void villages(GameTestHelper h) {
+ var level = h.getLevel();
+ var templates = level.getStructureManager();
+ int index = 0;
+ for (var house : MasonKilnProcessor.HOUSES.entrySet()) {
+ for (var rotation : Rotation.values()) {
+ var origin = h.absolutePos(new BlockPos(30 + index++ * 20, 3, 30));
+ var template = templates.getOrCreate(house.getKey());
+ var settings = new StructurePlaceSettings().setRotation(rotation);
+ var box = template.getBoundingBox(settings, origin);
+ var pool = StructurePoolElement.legacy(house.getKey().toString()).apply(StructureTemplatePool.Projection.RIGID);
+ check(h, pool.place(templates, level, level.structureManager(), level.getChunkSource().getGenerator(),
+ origin, origin, rotation, box, level.getRandom(), LiquidSettings.IGNORE_WATERLOGGING, false),
+ "Mason house must place through the real pool path: " + house.getKey());
+ var kilnPos = net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate
+ .calculateRelativePosition(settings, house.getValue().target()).offset(origin);
+ check(h, level.getBlockState(kilnPos).is(org.polaris2023.wildwind.core.block.ModBlocks.KILN.get()) && level.getBlockEntity(kilnPos) instanceof KilnBlockEntity,
+ "Every mason house rotation must contain a functional kiln: " + house.getKey() + " " + rotation);
+ check(h, level.getBlockState(kilnPos).getValue(AbstractFurnaceBlock.FACING) == rotation.rotate(house.getValue().facing()),
+ "Kiln facing must rotate with its house");
+ for (var cutter : template.filterBlocks(origin, settings, Blocks.STONECUTTER)) {
+ check(h, level.getBlockState(cutter.pos()).is(Blocks.STONECUTTER), "Mason workstation must be preserved");
+ }
+ }
+ }
+ h.succeed();
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/hfas/test/MuddyPigGameTests.java b/src/verification/java/org/polaris2023/wildwind/hfas/test/MuddyPigGameTests.java
new file mode 100644
index 0000000..93b02ec
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/hfas/test/MuddyPigGameTests.java
@@ -0,0 +1,106 @@
+package org.polaris2023.wildwind.hfas.test;
+
+import java.util.function.Consumer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.entity.EntitySpawnReason;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.entity.MobCategory;
+import net.minecraft.world.entity.SpawnPlacements;
+import net.minecraft.world.entity.ai.attributes.Attributes;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.SpawnEggItem;
+import net.minecraft.world.level.biome.Biomes;
+import net.minecraft.world.level.block.Blocks;
+import net.neoforged.bus.api.IEventBus;
+import net.neoforged.fml.common.Mod;
+import net.neoforged.neoforge.registries.DeferredRegister;
+import org.polaris2023.wildwind.wilderness.registry.ModEntities;
+import org.polaris2023.wildwind.structures.test.StructureGameTests;
+
+@Mod("wild_wind_tests")
+public final class MuddyPigGameTests {
+ public MuddyPigGameTests(IEventBus bus) {
+ StructureGameTests.register(bus);
+ GameplayGameTests.register(bus);
+ DeferredRegister> functions = DeferredRegister.create(Registries.TEST_FUNCTION, "wild_wind_tests");
+ functions.register("entity", () -> MuddyPigGameTests::entity);
+ functions.register("spawning", () -> MuddyPigGameTests::spawning);
+ functions.register(bus);
+ }
+
+ private static void check(GameTestHelper helper, boolean condition, String message) {
+ helper.assertTrue(condition, Component.literal(message));
+ }
+
+ private static void entity(GameTestHelper helper) {
+ var level = helper.getLevel();
+ var type = ModEntities.MUDDY_PIG.get();
+ var pig = helper.spawn(type, 2, 2, 2);
+ var partner = helper.spawn(type, 4, 2, 2);
+ check(helper, pig.getMaxHealth() == 10 && pig.getAttributeValue(Attributes.MOVEMENT_SPEED) == 0.25,
+ "Muddy pig must receive vanilla pig attributes");
+ check(helper, pig.isFood(Items.CARROT.getDefaultInstance()) && !pig.isFood(Items.WHEAT.getDefaultInstance()),
+ "Muddy pig must use vanilla pig food tag");
+ check(helper, SpawnEggItem.getType(org.polaris2023.wildwind.wilderness.registry.ModItems.MUDDY_PIG_SPAWN_EGG.toStack()) == type,
+ "Spawn egg entity component must point to muddy pig");
+ var baby = pig.getBreedOffspring(level, partner);
+ check(helper, baby != null && baby.getType() == type, "Breeding must create a muddy pig rather than vanilla pig");
+ baby.setBaby(true);
+ check(helper, baby.isBaby(), "Offspring must support baby state");
+ pig.setInLove(null);
+ partner.setInLove(null);
+ check(helper, pig.canMate(partner) && !pig.canMate(pig), "Two adult muddy pigs must be able to mate");
+ var vanilla = helper.spawn(EntityTypes.PIG, 6, 2, 2);
+ vanilla.setInLove(null);
+ check(helper, !pig.canMate(vanilla) && !vanilla.canMate(pig), "Vanilla pigs must remain a separate breeding species");
+ var eggBaby = SpawnEggItem.spawnOffspringFromSpawnEgg(helper.makeMockServerPlayerInLevel(), pig,
+ type, level, pig.position(), org.polaris2023.wildwind.wilderness.registry.ModItems.MUDDY_PIG_SPAWN_EGG.toStack());
+ check(helper, eggBaby.isPresent() && eggBaby.get().isBaby() && eggBaby.get().getType() == type,
+ "Using the spawn egg on an adult must create a muddy pig baby");
+ check(helper, level.getServer().reloadableRegistries().getLootTable(pig.getLootTable().orElseThrow())
+ != net.minecraft.world.level.storage.loot.LootTable.EMPTY, "Muddy pig loot table must be loaded");
+ helper.succeed();
+ }
+
+ private static void spawning(GameTestHelper helper) {
+ var level = helper.getLevel();
+ var type = ModEntities.MUDDY_PIG.get();
+ var biomes = level.registryAccess().lookupOrThrow(Registries.BIOME);
+ for (var biome : biomes.listElements().toList()) {
+ var entries = biome.value().getMobSettings().getMobs(MobCategory.CREATURE).unwrap();
+ var matches = entries.stream().filter(entry -> entry.value().type() == type).toList();
+ boolean expected = biome.is(Biomes.SWAMP) || biome.is(Biomes.MANGROVE_SWAMP);
+ check(helper, matches.size() == (expected ? 1 : 0), "Biome spawn membership: " + biome.key().identifier());
+ if (expected) {
+ var entry = matches.getFirst();
+ check(helper, entry.weight() == 10 && entry.value().minCount() == 2 && entry.value().maxCount() == 4,
+ "Spawn weight and group bounds must be 10 / 2-4");
+ }
+ }
+ var pos = new BlockPos(3, 2, 3);
+ var absolute = helper.absolutePos(pos);
+ helper.setBlock(pos, Blocks.AIR);
+ helper.setBlock(pos.above(), Blocks.AIR);
+ helper.setBlock(pos.above(2), Blocks.GLOWSTONE);
+ helper.runAfterDelay(5, () -> {
+ for (var block : new net.minecraft.world.level.block.Block[]{Blocks.GRASS_BLOCK, Blocks.MUD, Blocks.MUDDY_MANGROVE_ROOTS}) {
+ helper.setBlock(pos.below(), block);
+ check(helper, SpawnPlacements.isSpawnPositionOk(type, level, absolute)
+ && SpawnPlacements.checkSpawnRules(type, level, EntitySpawnReason.NATURAL, absolute, level.getRandom()),
+ "Natural spawning must accept " + block);
+ }
+ helper.setBlock(pos.below(), Blocks.STONE);
+ check(helper, !SpawnPlacements.checkSpawnRules(type, level, EntitySpawnReason.NATURAL, absolute, level.getRandom()),
+ "Natural spawning must reject stone");
+ helper.setBlock(pos.below(), Blocks.MUD);
+ helper.setBlock(pos, Blocks.WATER);
+ check(helper, !SpawnPlacements.isSpawnPositionOk(type, level, absolute), "Natural spawning must reject water");
+ helper.setBlock(pos, Blocks.STONE);
+ check(helper, !SpawnPlacements.isSpawnPositionOk(type, level, absolute), "Natural spawning must reject obstructed space");
+ helper.succeed();
+ });
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/hfas/test/SkeletonAndFarmlandGameTests.java b/src/verification/java/org/polaris2023/wildwind/hfas/test/SkeletonAndFarmlandGameTests.java
new file mode 100644
index 0000000..98964bf
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/hfas/test/SkeletonAndFarmlandGameTests.java
@@ -0,0 +1,154 @@
+package org.polaris2023.wildwind.hfas.test;
+
+import java.util.List;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Holder;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.effect.MobEffect;
+import net.minecraft.world.effect.MobEffectInstance;
+import net.minecraft.world.effect.MobEffects;
+import net.minecraft.world.entity.EntitySpawnReason;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.entity.ai.goal.MeleeAttackGoal;
+import net.minecraft.world.entity.monster.skeleton.AbstractSkeleton;
+import net.minecraft.world.item.CreativeModeTab;
+import net.minecraft.world.item.CreativeModeTabs;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.alchemy.PotionContents;
+import net.minecraft.world.item.enchantment.Enchantments;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.FarmlandBlock;
+import org.polaris2023.wildwind.core.CoreMod;
+import org.polaris2023.wildwind.core.registry.ModCreativeTabs;
+import org.polaris2023.wildwind.wilderness.registry.ModPotions;
+
+final class SkeletonAndFarmlandGameTests {
+ private SkeletonAndFarmlandGameTests() {}
+
+ private static void check(GameTestHelper h, boolean ok, String message) {
+ h.assertTrue(ok, Component.literal(message));
+ }
+
+ static void skeletonSpawns(GameTestHelper h) {
+ var level = h.getLevel();
+ level.getRandom().setSeed(428136L);
+ for (var type : List.of(EntityTypes.SKELETON, EntityTypes.STRAY, EntityTypes.BOGGED, EntityTypes.PARCHED)) {
+ int swords = 0;
+ for (int i = 0; i < 200; i++) {
+ // Use the real initialization path used by spawn eggs and newly spawned mobs.
+ var skeleton = type.create(level, null, h.absolutePos(new BlockPos(2, 2, 2)), EntitySpawnReason.NATURAL, false, false);
+ check(h, skeleton != null, "Skeleton variant must initialize");
+ var weapon = skeleton.getMainHandItem();
+ check(h, weapon.is(Items.BOW) || weapon.is(Items.STONE_SWORD), "Spawn weapon must be bow or stone sword");
+ boolean melee = skeleton.getGoalSelector().getAvailableGoals().stream()
+ .anyMatch(goal -> goal.getGoal() instanceof MeleeAttackGoal);
+ check(h, melee == weapon.is(Items.STONE_SWORD), "Weapon must select the matching combat AI");
+ if (weapon.is(Items.STONE_SWORD)) swords++;
+ skeleton.discard();
+ }
+ check(h, swords >= 5 && swords <= 45, "Fixed-seed spawn sample must contain roughly ten percent swords: " + type + " = " + swords);
+ }
+ var wither = EntityTypes.WITHER_SKELETON.create(level, null, h.absolutePos(new BlockPos(2, 2, 2)), EntitySpawnReason.NATURAL, false, false);
+ check(h, wither.getMainHandItem().is(Items.STONE_SWORD), "Wither skeleton must retain its vanilla equipment");
+ wither.discard();
+ h.succeed();
+ }
+
+ static void skeletonMelee(GameTestHelper h) {
+ checkHit(h, h.spawn(EntityTypes.STRAY, 2, 2, 2), MobEffects.SLOWNESS, 600);
+ checkHit(h, h.spawn(EntityTypes.BOGGED, 3, 2, 2), MobEffects.POISON, 80);
+ checkHit(h, h.spawn(EntityTypes.PARCHED, 4, 2, 2), MobEffects.WEAKNESS, 600);
+ var skeleton = h.spawn(EntityTypes.SKELETON, 5, 2, 2);
+ skeleton.setNoAi(true);
+ skeleton.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.STONE_SWORD));
+ var target = h.spawn(EntityTypes.COW, 5, 2, 4);
+ target.setNoAi(true);
+ check(h, skeleton.doHurtTarget(h.getLevel(), target) && target.getActiveEffects().isEmpty(), "Normal skeleton melee must not add a special effect");
+ h.succeed();
+ }
+
+ private static void checkHit(GameTestHelper h, AbstractSkeleton skeleton, Holder effect, int ticks) {
+ skeleton.setNoAi(true);
+ skeleton.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.STONE_SWORD));
+ var target = h.spawn(EntityTypes.COW, 2, 2, 5);
+ target.setNoAi(true);
+ check(h, skeleton.doHurtTarget(h.getLevel(), target), "Stone sword must successfully damage target");
+ var applied = target.getEffect(effect);
+ check(h, applied != null && applied.getDuration() == ticks && applied.getAmplifier() == 0,
+ "Melee must apply exactly the requested duration and level");
+ var invulnerable = h.spawn(EntityTypes.COW, 3, 2, 5);
+ invulnerable.setNoAi(true);
+ invulnerable.setInvulnerable(true);
+ check(h, !skeleton.doHurtTarget(h.getLevel(), invulnerable) && !invulnerable.hasEffect(effect),
+ "Failed melee hits must not apply an effect");
+ }
+
+ static void farmland(GameTestHelper h) {
+ var level = h.getLevel();
+ var cow = h.spawn(EntityTypes.COW, 2, 2, 2);
+ cow.setNoAi(true);
+ var pos = h.absolutePos(new BlockPos(3, 1, 3));
+ var farmland = (FarmlandBlock) Blocks.FARMLAND;
+ var featherFalling = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.FEATHER_FALLING);
+ var boots = new ItemStack(Items.DIAMOND_BOOTS);
+ boots.enchant(featherFalling, 1);
+ cow.setItemSlot(EquipmentSlot.FEET, boots);
+ level.setBlockAndUpdate(pos, Blocks.FARMLAND.defaultBlockState());
+ farmland.fallOn(level, level.getBlockState(pos), pos, cow, 6);
+ check(h, level.getBlockState(pos).is(Blocks.FARMLAND), "Worn Feather Falling I must protect farmland for non-player entities");
+ check(h, cow.getHealth() < cow.getMaxHealth(), "Farmland protection must not cancel remaining fall damage");
+ cow.setItemSlot(EquipmentSlot.FEET, ItemStack.EMPTY);
+ cow.setItemSlot(EquipmentSlot.MAINHAND, boots);
+ farmland.fallOn(level, level.getBlockState(pos), pos, cow, 6);
+ check(h, level.getBlockState(pos).is(Blocks.DIRT), "Holding enchanted armor must not protect farmland");
+ level.setBlockAndUpdate(pos, Blocks.FARMLAND.defaultBlockState());
+ cow.addEffect(new MobEffectInstance(MobEffects.SLOW_FALLING, 200));
+ farmland.fallOn(level, level.getBlockState(pos), pos, cow, 6);
+ check(h, level.getBlockState(pos).is(Blocks.FARMLAND), "Slow Falling must protect farmland without worn armor");
+ cow.removeEffect(MobEffects.SLOW_FALLING);
+ farmland.fallOn(level, level.getBlockState(pos), pos, cow, 6);
+ check(h, level.getBlockState(pos).is(Blocks.DIRT), "Removing protection must restore trampling");
+ h.succeed();
+ }
+
+ static void creativeTab(GameTestHelper h) {
+ var params = new CreativeModeTab.ItemDisplayParameters(h.getLevel().enabledFeatures(), true, h.getLevel().registryAccess());
+ var tab = ModCreativeTabs.WILD_WIND.get();
+ tab.buildContents(params);
+ for (var register : List.of(org.polaris2023.wildwind.core.block.ModBlocks.ITEMS, org.polaris2023.wildwind.core.registry.ModItems.ITEMS, org.polaris2023.wildwind.wilderness.block.ModBlocks.ITEMS, org.polaris2023.wildwind.wilderness.registry.ModItems.ITEMS, org.polaris2023.wildwind.barren.block.ModBlocks.ITEMS)) {
+ for (var item : register.getEntries()) {
+ check(h, tab.getDisplayItems().stream().filter(stack -> stack.is(item.get())).count() == (item == org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW ? 2 : 1),
+ "Every registered mod item/arrow variant must appear once in its own tab: " + item.getId());
+ }
+ }
+ for (var potion : ModPotions.POTIONS.getEntries()) {
+ for (var item : List.of(Items.POTION, Items.SPLASH_POTION, Items.LINGERING_POTION, Items.TIPPED_ARROW)) {
+ check(h, tab.contains(org.polaris2023.wildwind.wilderness.arrow.ReachArrows.convert(PotionContents.createItemStack(item, potion))), "Mod tab must include each Reach potion and arrow variant");
+ }
+ }
+ for (var key : List.of(CreativeModeTabs.BUILDING_BLOCKS, CreativeModeTabs.NATURAL_BLOCKS,
+ CreativeModeTabs.INGREDIENTS, CreativeModeTabs.SPAWN_EGGS, CreativeModeTabs.FOOD_AND_DRINKS, CreativeModeTabs.COMBAT)) {
+ var vanilla = BuiltInRegistries.CREATIVE_MODE_TAB.getValueOrThrow(key);
+ vanilla.buildContents(params);
+ check(h, vanilla.getDisplayItems().stream().noneMatch(stack ->
+ org.polaris2023.wildwind.core.api.WildWindModules.owns(BuiltInRegistries.ITEM.getKey(stack.getItem()).getNamespace())),
+ "Vanilla categories must not contain mod items");
+ check(h, vanilla.getDisplayItems().stream().noneMatch(stack -> {
+ var contents = stack.get(DataComponents.POTION_CONTENTS);
+ return contents != null && contents.potion().isPresent()
+ && org.polaris2023.wildwind.core.api.WildWindModules.owns(contents.potion().get().unwrapKey().orElseThrow().identifier().getNamespace());
+ }), "Vanilla categories must not duplicate mod potions");
+ }
+ CreativeModeTabs.searchTab().buildContents(params);
+ check(h, CreativeModeTabs.searchTab().contains(org.polaris2023.wildwind.wilderness.registry.ModItems.MUDDY_PIG_SPAWN_EGG.toStack())
+ && CreativeModeTabs.searchTab().contains(PotionContents.createItemStack(Items.POTION, ModPotions.REACH)),
+ "Global creative search must still discover mod contents");
+ h.succeed();
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/hfas/test/UpstreamIntegrationGameTests.java b/src/verification/java/org/polaris2023/wildwind/hfas/test/UpstreamIntegrationGameTests.java
new file mode 100644
index 0000000..1348f5a
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/hfas/test/UpstreamIntegrationGameTests.java
@@ -0,0 +1,84 @@
+package org.polaris2023.wildwind.hfas.test;
+
+import java.util.List;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.effect.MobEffectInstance;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.crafting.RecipeType;
+import net.minecraft.world.item.crafting.SingleRecipeInput;
+import net.minecraft.world.level.GameType;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.Vec3;
+import org.polaris2023.wildwind.wilderness.component.OmniClawTools;
+import org.polaris2023.wildwind.core.enchantment.MeltingEnchantment;
+import org.polaris2023.wildwind.wilderness.entity.animal.MudcrabVariant;
+import org.polaris2023.wildwind.wilderness.registry.*;
+
+final class UpstreamIntegrationGameTests {
+ private UpstreamIntegrationGameTests() {}
+
+ private static void check(GameTestHelper h, boolean ok, String message) {
+ h.assertTrue(ok, Component.literal(message));
+ }
+
+ static void run(GameTestHelper h) {
+ var level = h.getLevel();
+ var crab = h.spawn(ModEntities.MUDCRAB.get(), 2, 2, 2);
+ crab.setNoAi(true);
+ check(h, crab.getMaxHealth() == 10 && crab.getVariant() != null,
+ "Merged mudcrab must spawn with attributes and a registered variant");
+ check(h, level.registryAccess().lookupOrThrow(DatadrivenRegistryKey.MUDCRAB_VARIANT).size() == 3,
+ "All three mudcrab variants must be present");
+ crab.setVariant(MudcrabVariant.lookup(level.registryAccess(), MudcrabVariant.WARM));
+ check(h, crab.getVariant().is(MudcrabVariant.WARM)
+ && crab.getBucketItemStack().is(org.polaris2023.wildwind.wilderness.registry.ModItems.MUDCRAB_BUCKET.get()), "Mudcrab variant and bucket must work");
+ var fish = h.spawn(ModEntities.PIRANHA.get(), 4, 2, 2);
+ fish.setNoAi(true);
+ check(h, fish.getBucketItemStack().is(org.polaris2023.wildwind.wilderness.registry.ModItems.PIRANHA_BUCKET.get()), "Piranha bucket must remain registered");
+ var cooking = level.recipeAccess().getRecipeFor(RecipeType.SMELTING,
+ new SingleRecipeInput(org.polaris2023.wildwind.wilderness.registry.ModItems.PIRANHA.toStack()), level);
+ check(h, cooking.isPresent() && cooking.get().value().assemble(new SingleRecipeInput(org.polaris2023.wildwind.wilderness.registry.ModItems.PIRANHA.toStack()))
+ .is(org.polaris2023.wildwind.wilderness.registry.ModItems.COOKED_PIRANHA.get()), "Upstream piranha smelting recipe must load");
+ for (var material : org.polaris2023.wildwind.core.registry.ModItems.MATERIALS) {
+ check(h, !material.toStack().has(DataComponents.FOOD), "The seven local materials must stay ordinary items");
+ }
+ check(h, !ModPotions.REACH.value().getEffects().isEmpty(), "Upstream Reach potion must remain registered");
+
+ var player = (ServerPlayer) h.makeMockServerPlayer(GameType.SURVIVAL);
+ var connection = new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);
+ new io.netty.channel.embedded.EmbeddedChannel(connection);
+ level.getServer().getPlayerList().placeNewPlayer(connection, player,
+ net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(), false));
+ player.gameMode.changeGameModeForPlayer(GameType.SURVIVAL);
+ player.setPos(h.absoluteVec(new Vec3(6.5, 2, 6.5)));
+ player.addEffect(new MobEffectInstance(ModMobEffects.REACH, 200));
+ check(h, player.getAttributeValue(ModAttributes.EXTRA_ITEM_PICKUP_RANGE) == 2,
+ "Upstream Reach attribute must work with merged player code");
+ player.removeEffect(ModMobEffects.REACH);
+
+ var pickaxe = new ItemStack(Items.DIAMOND_PICKAXE);
+ pickaxe.enchant(level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(MeltingEnchantment.MELTING), 1);
+ var claw = org.polaris2023.wildwind.wilderness.registry.ModItems.OMNI_CLAW.toStack();
+ claw.set(ModDataComponents.OMNI_CLAW_TOOLS.get(), new OmniClawTools(List.of(pickaxe), 0));
+ player.setItemInHand(InteractionHand.MAIN_HAND, claw);
+ player.setExperienceLevels(1);
+ player.setExperiencePoints(0);
+ var pos = new BlockPos(5, 2, 5);
+ h.setBlock(pos, Blocks.IRON_ORE);
+ check(h, player.gameMode.destroyBlock(h.absolutePos(pos)), "Omni Claw must break a real block through the player path");
+ check(h, MeltingEnchantment.availableExperience(player) == 6,
+ "Melting on an Omni Claw's internal pickaxe must consume exactly one XP point");
+ check(h, level.getEntitiesOfClass(ItemEntity.class, new AABB(h.absolutePos(pos)).inflate(1)).stream()
+ .anyMatch(e -> e.getItem().is(Items.IRON_INGOT)), "Omni Claw and Melting must produce smelted loot together");
+ h.succeed();
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/structures/test/StructureGameTests.java b/src/verification/java/org/polaris2023/wildwind/structures/test/StructureGameTests.java
new file mode 100644
index 0000000..c16527a
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/structures/test/StructureGameTests.java
@@ -0,0 +1,99 @@
+package org.polaris2023.wildwind.structures.test;
+
+import java.util.function.Consumer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.HolderSet;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.Identifier;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.phys.Vec3;
+import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
+import net.neoforged.bus.api.IEventBus;
+import net.neoforged.neoforge.registries.DeferredRegister;
+import org.polaris2023.wildwind.structures.worldgen.ConfiguredStructure;
+import org.polaris2023.wildwind.structures.worldgen.ConfiguredTemplatePiece;
+import org.polaris2023.wildwind.structures.worldgen.GeneratedStructureIndex;
+
+public final class StructureGameTests {
+ private StructureGameTests() {}
+
+ public static void register(IEventBus bus) {
+ DeferredRegister> functions = DeferredRegister.create(Registries.TEST_FUNCTION, "wild_wind_tests");
+ functions.register("generation", () -> StructureGameTests::generation);
+ functions.register(bus);
+ }
+
+ private static void generation(GameTestHelper helper) {
+ helper.assertTrue(net.neoforged.fml.ModList.get().isLoaded("wild_wind_core")
+ && !net.neoforged.fml.ModList.get().isLoaded("ww_structures"),
+ Component.literal("Structures and items must load from the core mod"));
+ for (var item : org.polaris2023.wildwind.core.registry.ModItems.MATERIALS) {
+ helper.assertTrue(net.minecraft.core.registries.BuiltInRegistries.ITEM.containsKey(item.getId())
+ && item.get().getClass() == net.minecraft.world.item.Item.class
+ && !item.toStack().has(net.minecraft.core.component.DataComponents.FOOD),
+ Component.literal("Plain material must be available alongside structures: " + item.getId()));
+ }
+ var level = helper.getLevel();
+ var id = Identifier.parse("ww_structures:smoke");
+ var holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(ResourceKey.create(Registries.STRUCTURE, id));
+ helper.assertTrue(holder.value() instanceof ConfiguredStructure, Component.literal("Config must become a registered structure"));
+ for (String rejected : new String[]{"blocked", "disabled"}) {
+ var rejectedHolder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(
+ ResourceKey.create(Registries.STRUCTURE, Identifier.parse("ww_structures:" + rejected)));
+ helper.assertTrue(level.getChunkSource().getGenerator().findNearestMapStructure(level, HolderSet.direct(rejectedHolder),
+ new BlockPos(2048, 0, 2048), 2, false) == null, Component.literal(rejected + " must not generate"));
+ }
+ // Far from the test arena; generate through the actual ChunkGenerator, not /place.
+ var result = level.getChunkSource().getGenerator().findNearestMapStructure(level, HolderSet.direct(holder),
+ new BlockPos(2048, 0, 2048), 4, false);
+ helper.assertTrue(result != null, Component.literal("Native locate must discover the configured structure"));
+ var pos = result.getFirst();
+ var chunk = level.getChunk(pos);
+ var start = chunk.getStartForStructure(holder.value());
+ helper.assertTrue(start != null && start.isValid(), Component.literal("Generated chunk must contain a valid start"));
+ var piece = (ConfiguredTemplatePiece) start.getPieces().getFirst();
+ var box = piece.getBoundingBox();
+ helper.assertTrue(box.getXSpan() == 19 && box.getZSpan() == 23, Component.literal("Rotation must swap template footprint"));
+ BlockPos floor = new BlockPos(box.minX(), box.minY(), box.minZ());
+ // Load every intersecting chunk so that the cross-chunk template has been placed completely.
+ for (int x = box.minX() >> 4; x <= box.maxX() >> 4; x++) {
+ for (int z = box.minZ() >> 4; z <= box.maxZ() >> 4; z++) level.getChunk(x, z);
+ }
+ helper.assertTrue(level.getBlockState(floor).is(Blocks.MOSSY_COBBLESTONE), Component.literal("Template foundation must generate"));
+ helper.assertTrue(level.getBlockState(floor.above(1)).is(Blocks.STONE_BRICKS), Component.literal("Template pillar must generate"));
+ for (int x : new int[]{box.minX(), box.maxX()}) {
+ for (int z : new int[]{box.minZ(), box.maxZ()}) {
+ helper.assertTrue(level.getBlockState(new BlockPos(x, box.minY(), z)).is(Blocks.MOSSY_COBBLESTONE),
+ Component.literal("All corners must generate across chunk boundaries"));
+ helper.assertTrue(level.getBlockState(new BlockPos(x, box.minY() + 1, z)).is(Blocks.STONE_BRICKS),
+ Component.literal("Rotated pillars must generate across chunk boundaries"));
+ }
+ }
+ var serialization = StructurePieceSerializationContext.fromLevel(level);
+ CompoundTag saved = piece.createTag(serialization);
+ var restored = new ConfiguredTemplatePiece(serialization, saved);
+ helper.assertTrue(restored.templatePosition().equals(piece.templatePosition()) && restored.getRotation() == piece.getRotation(),
+ Component.literal("Piece origin and rotation must survive chunk NBT round trip"));
+ helper.runAfterDelay(2, () -> {
+ var index = level.getDataStorage().computeIfAbsent(GeneratedStructureIndex.TYPE);
+ helper.assertTrue(index.nearest(id, pos).isPresent(), Component.literal("Full chunk must enter the generated structure index"));
+ var source = level.getServer().createCommandSourceStack().withLevel(level);
+ var dispatcher = level.getServer().getCommands().getDispatcher();
+ try {
+ helper.assertTrue(dispatcher.execute("wwstructure locate smoke", source.withPosition(Vec3.atCenterOf(pos))) == 1,
+ Component.literal("Generated locate command must succeed"));
+ helper.assertTrue(dispatcher.execute("wwstructure search smoke 2", source.withPosition(Vec3.atCenterOf(pos))) == 1,
+ Component.literal("Native search command must succeed"));
+ helper.assertTrue(dispatcher.execute("wwstructure locate missing", source) == 0,
+ Component.literal("Unknown structure must report no result"));
+ } catch (com.mojang.brigadier.exceptions.CommandSyntaxException exception) {
+ throw new IllegalStateException(exception);
+ }
+ helper.succeed();
+ });
+ }
+}
diff --git a/src/verification/java/org/polaris2023/wildwind/structures/test/mixin/GameTestServerMixin.java b/src/verification/java/org/polaris2023/wildwind/structures/test/mixin/GameTestServerMixin.java
new file mode 100644
index 0000000..6b86b94
--- /dev/null
+++ b/src/verification/java/org/polaris2023/wildwind/structures/test/mixin/GameTestServerMixin.java
@@ -0,0 +1,14 @@
+package org.polaris2023.wildwind.structures.test.mixin;
+
+import net.minecraft.gametest.framework.GameTestServer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyArg;
+
+/** Vanilla GameTest disables structure generation. Enable it only in the test mod. */
+@Mixin(GameTestServer.class)
+public abstract class GameTestServerMixin {
+ @ModifyArg(method = "", at = @At(value = "INVOKE",
+ target = "Lnet/minecraft/world/level/levelgen/WorldOptions;(JZZ)V"), index = 1)
+ private static boolean structuresTests$enableStructures(boolean original) { return true; }
+}
diff --git a/src/verification/resources/data/minecraft/worldgen/world_preset/flat.json b/src/verification/resources/data/minecraft/worldgen/world_preset/flat.json
new file mode 100644
index 0000000..e53cbdd
--- /dev/null
+++ b/src/verification/resources/data/minecraft/worldgen/world_preset/flat.json
@@ -0,0 +1,21 @@
+{
+ "dimensions": {
+ "minecraft:overworld": {
+ "type": "minecraft:overworld",
+ "generator": {
+ "type": "minecraft:flat",
+ "settings": {
+ "biome": "minecraft:plains",
+ "features": false,
+ "lakes": false,
+ "layers": [
+ {"block": "minecraft:bedrock", "height": 1},
+ {"block": "minecraft:dirt", "height": 2},
+ {"block": "minecraft:grass_block", "height": 1}
+ ],
+ "structure_overrides": ["ww_structures:smoke", "ww_structures:blocked"]
+ }
+ }
+ }
+ }
+}
diff --git a/src/verification/resources/data/wild_wind_tests/structure/empty.nbt b/src/verification/resources/data/wild_wind_tests/structure/empty.nbt
new file mode 100644
index 0000000..1b63d36
Binary files /dev/null and b/src/verification/resources/data/wild_wind_tests/structure/empty.nbt differ
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/azalea_shelf.json b/src/verification/resources/data/wild_wind_tests/test_instance/azalea_shelf.json
new file mode 100644
index 0000000..eca6cef
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/azalea_shelf.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:azalea_shelf",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/azalea_shelf_chain.json b/src/verification/resources/data/wild_wind_tests/test_instance/azalea_shelf_chain.json
new file mode 100644
index 0000000..df660b0
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/azalea_shelf_chain.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:azalea_shelf_chain",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/azalea_signs.json b/src/verification/resources/data/wild_wind_tests/test_instance/azalea_signs.json
new file mode 100644
index 0000000..2052346
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/azalea_signs.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:azalea_signs",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/cobweb_tick_event.json b/src/verification/resources/data/wild_wind_tests/test_instance/cobweb_tick_event.json
new file mode 100644
index 0000000..46a68dc
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/cobweb_tick_event.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:cobweb_tick_event",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/cobweb_timers.json b/src/verification/resources/data/wild_wind_tests/test_instance/cobweb_timers.json
new file mode 100644
index 0000000..0a49730
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/cobweb_timers.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:cobweb_timers",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/crafter_shears.json b/src/verification/resources/data/wild_wind_tests/test_instance/crafter_shears.json
new file mode 100644
index 0000000..20acd51
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/crafter_shears.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:crafter_shears",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/creative_tab.json b/src/verification/resources/data/wild_wind_tests/test_instance/creative_tab.json
new file mode 100644
index 0000000..651679a
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/creative_tab.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:creative_tab",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/entity.json b/src/verification/resources/data/wild_wind_tests/test_instance/entity.json
new file mode 100644
index 0000000..9cc0e61
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/entity.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:entity",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 100
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/farmland_protection.json b/src/verification/resources/data/wild_wind_tests/test_instance/farmland_protection.json
new file mode 100644
index 0000000..e773bc8
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/farmland_protection.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:farmland_protection",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/fluid_effects.json b/src/verification/resources/data/wild_wind_tests/test_instance/fluid_effects.json
new file mode 100644
index 0000000..8f0a0f4
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/fluid_effects.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:fluid_effects",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/fluid_entity_rules.json b/src/verification/resources/data/wild_wind_tests/test_instance/fluid_entity_rules.json
new file mode 100644
index 0000000..904f525
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/fluid_entity_rules.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:fluid_entity_rules",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/generation.json b/src/verification/resources/data/wild_wind_tests/test_instance/generation.json
new file mode 100644
index 0000000..15d7783
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/generation.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:generation",
+ "environment": "minecraft:default",
+ "structure": "ww_structures:smoke",
+ "max_ticks": 200
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/inspection_blocks.json b/src/verification/resources/data/wild_wind_tests/test_instance/inspection_blocks.json
new file mode 100644
index 0000000..03f98a3
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/inspection_blocks.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:inspection_blocks",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 100
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/inspection_interactions.json b/src/verification/resources/data/wild_wind_tests/test_instance/inspection_interactions.json
new file mode 100644
index 0000000..6cd8d78
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/inspection_interactions.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:inspection_interactions",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 100
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/inspection_sulfur.json b/src/verification/resources/data/wild_wind_tests/test_instance/inspection_sulfur.json
new file mode 100644
index 0000000..7ddc9fe
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/inspection_sulfur.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:inspection_sulfur",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 100
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/kiln.json b/src/verification/resources/data/wild_wind_tests/test_instance/kiln.json
new file mode 100644
index 0000000..0858c93
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/kiln.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:kiln",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/kiln_inventory.json b/src/verification/resources/data/wild_wind_tests/test_instance/kiln_inventory.json
new file mode 100644
index 0000000..54a61d7
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/kiln_inventory.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:kiln_inventory",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/kiln_villages.json b/src/verification/resources/data/wild_wind_tests/test_instance/kiln_villages.json
new file mode 100644
index 0000000..846dc6e
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/kiln_villages.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:kiln_villages",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/land_choke.json b/src/verification/resources/data/wild_wind_tests/test_instance/land_choke.json
new file mode 100644
index 0000000..6b24aef
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/land_choke.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:land_choke",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/melting.json b/src/verification/resources/data/wild_wind_tests/test_instance/melting.json
new file mode 100644
index 0000000..e5823ca
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/melting.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:melting",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/melting_table.json b/src/verification/resources/data/wild_wind_tests/test_instance/melting_table.json
new file mode 100644
index 0000000..6cd028a
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/melting_table.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:melting_table",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 100
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/reach_arrow_acquisition.json b/src/verification/resources/data/wild_wind_tests/test_instance/reach_arrow_acquisition.json
new file mode 100644
index 0000000..db79f5a
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/reach_arrow_acquisition.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:reach_arrow_acquisition",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/reach_arrows.json b/src/verification/resources/data/wild_wind_tests/test_instance/reach_arrows.json
new file mode 100644
index 0000000..15e6fd0
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/reach_arrows.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:reach_arrows",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/skeleton_melee.json b/src/verification/resources/data/wild_wind_tests/test_instance/skeleton_melee.json
new file mode 100644
index 0000000..6f507d9
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/skeleton_melee.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:skeleton_melee",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/skeleton_spawns.json b/src/verification/resources/data/wild_wind_tests/test_instance/skeleton_spawns.json
new file mode 100644
index 0000000..2286ef2
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/skeleton_spawns.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:skeleton_spawns",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/spawning.json b/src/verification/resources/data/wild_wind_tests/test_instance/spawning.json
new file mode 100644
index 0000000..c432495
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/spawning.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:spawning",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 100
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/upstream_integration.json b/src/verification/resources/data/wild_wind_tests/test_instance/upstream_integration.json
new file mode 100644
index 0000000..65247be
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/upstream_integration.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:upstream_integration",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/data/wild_wind_tests/test_instance/wool_recipe.json b/src/verification/resources/data/wild_wind_tests/test_instance/wool_recipe.json
new file mode 100644
index 0000000..1e76051
--- /dev/null
+++ b/src/verification/resources/data/wild_wind_tests/test_instance/wool_recipe.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "function": "wild_wind_tests:wool_recipe",
+ "environment": "minecraft:default",
+ "structure": "wild_wind_tests:empty",
+ "max_ticks": 240
+}
diff --git a/src/verification/resources/wild_wind_tests.mixins.json b/src/verification/resources/wild_wind_tests.mixins.json
new file mode 100644
index 0000000..cb18fe7
--- /dev/null
+++ b/src/verification/resources/wild_wind_tests.mixins.json
@@ -0,0 +1,7 @@
+{
+ "required": true,
+ "package": "org.polaris2023.wildwind.structures.test.mixin",
+ "compatibilityLevel": "JAVA_25",
+ "mixins": ["GameTestServerMixin"],
+ "injectors": {"defaultRequire": 1}
+}
diff --git a/src/wilderness/java/org/polaris2023/wildwind/wilderness/WildernessMod.java b/src/wilderness/java/org/polaris2023/wildwind/wilderness/WildernessMod.java
new file mode 100644
index 0000000..ebf3918
--- /dev/null
+++ b/src/wilderness/java/org/polaris2023/wildwind/wilderness/WildernessMod.java
@@ -0,0 +1,29 @@
+package org.polaris2023.wildwind.wilderness;
+import net.minecraft.resources.Identifier;
+import net.neoforged.bus.api.IEventBus;
+import net.neoforged.fml.common.Mod;
+import org.polaris2023.wildwind.wilderness.block.ModBlocks;
+import org.polaris2023.wildwind.wilderness.registry.*;
+
+@Mod(WildernessMod.MOD_ID)
+public final class WildernessMod {
+ public static final String MOD_ID = "wild_wind_wilderness";
+ public WildernessMod(IEventBus bus) {
+ ModBlocks.register(bus);
+ ModEntities.register(bus);
+ ModItems.register(bus);
+ org.polaris2023.wildwind.wilderness.effect.ModEffects.register(bus);
+ ModDataComponents.register(bus);
+ ModAttributes.register(bus);
+ ModMobEffects.register(bus);
+ ModPotions.register(bus);
+ ModSoundEvents.register(bus);
+ ModEntityDataSerializers.register(bus);
+ bus.addListener((net.neoforged.neoforge.registries.DataPackRegistryEvent.NewRegistry event) ->
+ event.dataPackRegistry(DatadrivenRegistryKey.MUDCRAB_VARIANT,
+ org.polaris2023.wildwind.wilderness.entity.animal.MudcrabVariant.DIRECT_CODEC,
+ org.polaris2023.wildwind.wilderness.entity.animal.MudcrabVariant.NETWORK_CODEC));
+
+ }
+ public static Identifier id(String path) { return Identifier.fromNamespaceAndPath(MOD_ID, path); }
+}
diff --git a/src/wilderness/java/org/polaris2023/wildwind/wilderness/arrow/ReachArrows.java b/src/wilderness/java/org/polaris2023/wildwind/wilderness/arrow/ReachArrows.java
new file mode 100644
index 0000000..d12b24f
--- /dev/null
+++ b/src/wilderness/java/org/polaris2023/wildwind/wilderness/arrow/ReachArrows.java
@@ -0,0 +1,19 @@
+package org.polaris2023.wildwind.wilderness.arrow;
+
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import org.polaris2023.wildwind.wilderness.registry.ModPotions;
+
+public final class ReachArrows {
+ private ReachArrows() {}
+
+ /** Preserve counts and potion/custom components when vanilla crafting or trades create Reach arrows. */
+ public static ItemStack convert(ItemStack stack) {
+ var potion = stack.get(DataComponents.POTION_CONTENTS);
+ if (!stack.is(Items.TIPPED_ARROW) || potion == null || potion.potion().isEmpty()) return stack;
+ var holder = potion.potion().get();
+ if (!holder.is(ModPotions.REACH.getKey()) && !holder.is(ModPotions.LONG_REACH.getKey())) return stack;
+ return stack.transmuteCopy(org.polaris2023.wildwind.wilderness.registry.ModItems.TIPPED_ARROW.get());
+ }
+}
diff --git a/src/hfas/java/org/polaris2023/wildwind/hfas/block/ModBlocks.java b/src/wilderness/java/org/polaris2023/wildwind/wilderness/block/ModBlocks.java
similarity index 62%
rename from src/hfas/java/org/polaris2023/wildwind/hfas/block/ModBlocks.java
rename to src/wilderness/java/org/polaris2023/wildwind/wilderness/block/ModBlocks.java
index 03ee298..729d416 100644
--- a/src/hfas/java/org/polaris2023/wildwind/hfas/block/ModBlocks.java
+++ b/src/wilderness/java/org/polaris2023/wildwind/wilderness/block/ModBlocks.java
@@ -1,4 +1,4 @@
-package org.polaris2023.wildwind.hfas.block;
+package org.polaris2023.wildwind.wilderness.block;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
@@ -12,8 +12,8 @@
import net.neoforged.neoforge.registries.DeferredBlock;
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;
-import org.polaris2023.wildwind.hfas.HFASMod;
-import org.polaris2023.wildwind.hfas.worldgen.ModTreeGrowers;
+import org.polaris2023.wildwind.wilderness.WildernessMod;
+import org.polaris2023.wildwind.wilderness.worldgen.ModTreeGrowers;
import java.util.ArrayList;
import java.util.List;
@@ -29,31 +29,16 @@
*/
//TODO 考虑迁移至registry目录下,或将registry目录下的注册条目分散到各目录下
public class ModBlocks {
- // 方块注册器
- public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(HFASMod.MOD_ID);
- // 物品注册器
- public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(HFASMod.MOD_ID);
-
- // ==================== 方块类型定义 ====================
-
- // 焚烬木方块类型
+ public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(WildernessMod.MOD_ID);
+ public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(WildernessMod.MOD_ID);
public static final BlockSetType BLAZE_BLOCK_SET = BlockSetType.register(new BlockSetType("blaze"));
- // 焚烬木木材类型
+
public static final WoodType BLAZE_WOOD_TYPE = WoodType.register(new WoodType("blaze", BLAZE_BLOCK_SET));
- // 灵焰木方块类型
public static final BlockSetType SOUL_BLOCK_SET = BlockSetType.register(new BlockSetType("soul"));
- // 灵焰木木材类型
- public static final WoodType SOUL_WOOD_TYPE = WoodType.register(new WoodType("soul", SOUL_BLOCK_SET));
-
- // 杜鹃木方块类型
- public static final BlockSetType AZALEA_BLOCK_SET = BlockSetType.register(new BlockSetType("azalea"));
- // 杜鹃木木材类型
- public static final WoodType AZALEA_WOOD_TYPE = WoodType.register(new WoodType("azalea", AZALEA_BLOCK_SET));
- // ==================== 焚烬木套件 (blaze_wood) ====================
+ public static final WoodType SOUL_WOOD_TYPE = WoodType.register(new WoodType("soul", SOUL_BLOCK_SET));
- // 焚烬木原木
public static final DeferredBlock BLAZE_LOG = BLOCKS.registerBlock(
"blaze_log",
p -> new RotatedPillarBlock(p
@@ -62,7 +47,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木
public static final DeferredBlock BLAZE_WOOD = BLOCKS.registerBlock(
"blaze_wood",
p -> new RotatedPillarBlock(p
@@ -71,7 +55,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 去皮焚烬木原木
public static final DeferredBlock STRIPPED_BLAZE_LOG = BLOCKS.registerBlock(
"stripped_blaze_log",
p -> new RotatedPillarBlock(p
@@ -80,7 +63,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 去皮焚烬木
public static final DeferredBlock STRIPPED_BLAZE_WOOD = BLOCKS.registerBlock(
"stripped_blaze_wood",
p -> new RotatedPillarBlock(p
@@ -89,7 +71,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木木板
public static final DeferredBlock BLAZE_PLANKS = BLOCKS.registerBlock(
"blaze_planks",
p -> new Block(p
@@ -98,7 +79,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木楼梯
public static final DeferredBlock BLAZE_STAIRS = BLOCKS.registerBlock(
"blaze_stairs",
p -> new StairBlock(BLAZE_PLANKS.get().defaultBlockState(), p
@@ -107,7 +87,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木台阶
public static final DeferredBlock BLAZE_SLAB = BLOCKS.registerBlock(
"blaze_slab",
p -> new SlabBlock(p
@@ -116,7 +95,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木栅栏
public static final DeferredBlock BLAZE_FENCE = BLOCKS.registerBlock(
"blaze_fence",
p -> new FenceBlock(p
@@ -125,7 +103,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木栅栏门
public static final DeferredBlock BLAZE_FENCE_GATE = BLOCKS.registerBlock(
"blaze_fence_gate",
p -> new FenceGateBlock(BLAZE_WOOD_TYPE, p
@@ -134,7 +111,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木门
public static final DeferredBlock BLAZE_DOOR = BLOCKS.registerBlock(
"blaze_door",
p -> new DoorBlock(BLAZE_BLOCK_SET, p
@@ -143,7 +119,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木活板门
public static final DeferredBlock BLAZE_TRAPDOOR = BLOCKS.registerBlock(
"blaze_trapdoor",
p -> new TrapDoorBlock(BLAZE_BLOCK_SET, p
@@ -152,7 +127,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木压力板
public static final DeferredBlock BLAZE_PRESSURE_PLATE = BLOCKS.registerBlock(
"blaze_pressure_plate",
p -> new PressurePlateBlock(BLAZE_BLOCK_SET, p
@@ -161,7 +135,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木按钮
public static final DeferredBlock BLAZE_BUTTON = BLOCKS.registerBlock(
"blaze_button",
p -> new ButtonBlock(BLAZE_BLOCK_SET, 30, p
@@ -170,10 +143,10 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 焚烬木树叶
public static final DeferredBlock BLAZE_LEAVES = BLOCKS.registerBlock(
"blaze_leaves",
- p -> new TintedParticleLeavesBlock(0.01F,p
+ p -> new UntintedParticleLeavesBlock(0.01F,
+ net.minecraft.core.particles.ColorParticleOption.create(net.minecraft.core.particles.ParticleTypes.TINTED_LEAVES, 0xFFFF8000), p
.mapColor(MapColor.COLOR_ORANGE)
.strength(0.2F)
.sound(SoundType.GRASS)
@@ -181,7 +154,6 @@ public class ModBlocks {
.randomTicks())
);
- // 焚烬木树苗
public static final DeferredBlock BLAZE_SAPLING = BLOCKS.registerBlock(
"blaze_sapling",
p -> new SaplingBlock(ModTreeGrowers.BLAZE, p
@@ -192,9 +164,6 @@ public class ModBlocks {
.randomTicks())
);
- // ==================== 灵焰木套件 (soul_wood) ====================
-
- // 灵焰木原木
public static final DeferredBlock SOUL_LOG = BLOCKS.registerBlock(
"soul_log",
p -> new RotatedPillarBlock(p
@@ -203,7 +172,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木
public static final DeferredBlock SOUL_WOOD = BLOCKS.registerBlock(
"soul_wood",
p -> new RotatedPillarBlock(p
@@ -212,7 +180,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 去皮灵焰木原木
public static final DeferredBlock STRIPPED_SOUL_LOG = BLOCKS.registerBlock(
"stripped_soul_log",
p -> new RotatedPillarBlock(p
@@ -221,7 +188,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 去皮灵焰木
public static final DeferredBlock STRIPPED_SOUL_WOOD = BLOCKS.registerBlock(
"stripped_soul_wood",
p -> new RotatedPillarBlock(p
@@ -230,7 +196,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木木板
public static final DeferredBlock SOUL_PLANKS = BLOCKS.registerBlock(
"soul_planks",
p -> new Block(p
@@ -239,7 +204,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木楼梯
public static final DeferredBlock SOUL_STAIRS = BLOCKS.registerBlock(
"soul_stairs",
p -> new StairBlock(SOUL_PLANKS.get().defaultBlockState(), p
@@ -248,7 +212,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木台阶
public static final DeferredBlock SOUL_SLAB = BLOCKS.registerBlock(
"soul_slab",
p -> new SlabBlock(p
@@ -257,7 +220,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木栅栏
public static final DeferredBlock SOUL_FENCE = BLOCKS.registerBlock(
"soul_fence",
p -> new FenceBlock(p
@@ -266,7 +228,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木栅栏门
public static final DeferredBlock SOUL_FENCE_GATE = BLOCKS.registerBlock(
"soul_fence_gate",
p -> new FenceGateBlock(SOUL_WOOD_TYPE, p
@@ -275,7 +236,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木门
public static final DeferredBlock SOUL_DOOR = BLOCKS.registerBlock(
"soul_door",
p -> new DoorBlock(SOUL_BLOCK_SET, p
@@ -284,7 +244,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木活板门
public static final DeferredBlock SOUL_TRAPDOOR = BLOCKS.registerBlock(
"soul_trapdoor",
p -> new TrapDoorBlock(SOUL_BLOCK_SET, p
@@ -293,7 +252,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木压力板
public static final DeferredBlock SOUL_PRESSURE_PLATE = BLOCKS.registerBlock(
"soul_pressure_plate",
p -> new PressurePlateBlock(SOUL_BLOCK_SET, p
@@ -302,7 +260,6 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木按钮
public static final DeferredBlock SOUL_BUTTON = BLOCKS.registerBlock(
"soul_button",
p -> new ButtonBlock(SOUL_BLOCK_SET, 30, p
@@ -311,10 +268,10 @@ public class ModBlocks {
.sound(SoundType.WOOD))
);
- // 灵焰木树叶
public static final DeferredBlock SOUL_LEAVES = BLOCKS.registerBlock(
"soul_leaves",
- p -> new TintedParticleLeavesBlock(0.01F, p
+ p -> new UntintedParticleLeavesBlock(0.01F,
+ net.minecraft.core.particles.ColorParticleOption.create(net.minecraft.core.particles.ParticleTypes.TINTED_LEAVES, 0xFF00FFFF), p
.mapColor(MapColor.COLOR_CYAN)
.strength(0.2F)
.sound(SoundType.GRASS)
@@ -322,7 +279,6 @@ public class ModBlocks {
.randomTicks())
);
- // 灵焰木树苗
public static final DeferredBlock SOUL_SAPLING = BLOCKS.registerBlock(
"soul_sapling",
p -> new SaplingBlock(ModTreeGrowers.SOUL, p
@@ -333,128 +289,6 @@ public class ModBlocks {
.randomTicks())
);
- // ==================== 杜鹃木套件 (azalea_wood) ====================
-
- // 杜鹃木原木
- public static final DeferredBlock AZALEA_LOG = BLOCKS.registerBlock(
- "azalea_log",
- p -> new RotatedPillarBlock(p
- .mapColor(MapColor.COLOR_LIGHT_GREEN)
- .strength(2.0F)
- .sound(SoundType.WOOD))
- );
-
- // 杜鹃木
- public static final DeferredBlock AZALEA_WOOD = BLOCKS.registerBlock(
- "azalea_wood",
- p -> new RotatedPillarBlock(p
- .mapColor(MapColor.COLOR_LIGHT_GREEN)
- .strength(2.0F)
- .sound(SoundType.WOOD))
- );
-
- // 去皮杜鹃木原木
- public static final DeferredBlock STRIPPED_AZALEA_LOG = BLOCKS.registerBlock(
- "stripped_azalea_log",
- p -> new RotatedPillarBlock(p
- .mapColor(MapColor.COLOR_LIGHT_GREEN)
- .strength(2.0F)
- .sound(SoundType.WOOD))
- );
-
- // 去皮杜鹃木
- public static final DeferredBlock