From e34f13e1fb30d31686ae06f35fdecbc4cdffd2d9 Mon Sep 17 00:00:00 2001 From: frostbite-bot <304862314+frostbite-bot@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:43:41 +0000 Subject: [PATCH 1/2] Add dimensional pig farming --- .../beyonddimensions/BeyondDimensions.java | 2 + .../api/dimensionnet/DimensionsNet.java | 52 ++++++- .../api/longtype/PigType.java | 38 +++++ .../api/storage/key/impl/PigStackKey.java | 147 ++++++++++++++++++ .../storage/key/render/PigStackKeyRender.java | 94 +++++++++++ .../common/item/NetMagnetItem.java | 12 ++ .../assets/beyonddimensions/lang/en_us.json | 3 +- .../assets/beyonddimensions/lang/zh_cn.json | 3 +- 8 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/wintercogs/beyonddimensions/api/longtype/PigType.java create mode 100644 src/main/java/com/wintercogs/beyonddimensions/api/storage/key/impl/PigStackKey.java create mode 100644 src/main/java/com/wintercogs/beyonddimensions/api/storage/key/render/PigStackKeyRender.java diff --git a/src/main/java/com/wintercogs/beyonddimensions/BeyondDimensions.java b/src/main/java/com/wintercogs/beyonddimensions/BeyondDimensions.java index e38b175c..3d0f6ad6 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/BeyondDimensions.java +++ b/src/main/java/com/wintercogs/beyonddimensions/BeyondDimensions.java @@ -18,6 +18,7 @@ import com.wintercogs.beyonddimensions.api.storage.key.impl.EnergyStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.FluidStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.ItemStackKey; +import com.wintercogs.beyonddimensions.api.storage.key.impl.PigStackKey; import com.wintercogs.beyonddimensions.common.init.*; import com.wintercogs.beyonddimensions.integration.IntegrationManager; import net.minecraft.resources.ResourceLocation; @@ -66,6 +67,7 @@ private void commonSetup(final FMLCommonSetupEvent event) StackKeyRegistry.registerType(ItemStackKey.EMPTY); StackKeyRegistry.registerType(FluidStackKey.EMPTY); StackKeyRegistry.registerType(EnergyStackKey.INSTANCE); + StackKeyRegistry.registerType(PigStackKey.INSTANCE); // 注册方块能力类型,用于动态为方块注册能力 CapabilityHelper.BlockCapabilityMap.put(ItemStackKey.ID, Capabilities.ItemHandler.BLOCK); diff --git a/src/main/java/com/wintercogs/beyonddimensions/api/dimensionnet/DimensionsNet.java b/src/main/java/com/wintercogs/beyonddimensions/api/dimensionnet/DimensionsNet.java index 3a384196..55da4cba 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/api/dimensionnet/DimensionsNet.java +++ b/src/main/java/com/wintercogs/beyonddimensions/api/dimensionnet/DimensionsNet.java @@ -5,6 +5,7 @@ import com.wintercogs.beyonddimensions.api.storage.key.KeyAmount; import com.wintercogs.beyonddimensions.api.storage.key.impl.EnergyStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.ItemStackKey; +import com.wintercogs.beyonddimensions.api.storage.key.impl.PigStackKey; import com.wintercogs.beyonddimensions.common.init.BDItems; import com.wintercogs.beyonddimensions.config.ServerConfigRuntime; import com.wintercogs.beyonddimensions.util.PlayerNameHelper; @@ -16,6 +17,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.level.saveddata.SavedData; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.neoforge.common.NeoForge; @@ -93,6 +95,13 @@ public class DimensionsNet extends SavedData */ private int currentTime = 0; + /** + * 维度养猪场繁殖进度。正常猪被喂食后约需5分钟才能再次繁殖, + * 因此网络每6000 tick消耗两根胡萝卜并增加一只猪。 + */ + private int pigBreedingTime = 0; + private static final int PIG_BREEDING_INTERVAL = 6000; + /** * 构造函数 * @@ -380,6 +389,7 @@ public static DimensionsNet load(CompoundTag tag, HolderLookup.Provider registry // 读取倒计时 net.currentTime = tag.getInt("currentTime"); + net.pigBreedingTime = tag.getInt("pigBreedingTime"); if (tag.contains("Deleted")) net.deleted = tag.getBoolean("Deleted"); @@ -428,6 +438,7 @@ public static DimensionsNet load(CompoundTag tag, HolderLookup.Provider registry // 保存倒计时 tag.putInt("currentTime", this.currentTime); + tag.putInt("pigBreedingTime", this.pigBreedingTime); // 保存删除状态 tag.putBoolean("Deleted", this.deleted); @@ -725,9 +736,11 @@ else if (isManager(playerId)) @SubscribeEvent public void onServerTick(ServerTickEvent.Pre event) { - // 不对临时网络执行倒计时 - if (temporary || ServerConfigRuntime.crystalGenerateTime <= 0) - return; + if (temporary) return; + + tickPigBreeding(); + + if (ServerConfigRuntime.crystalGenerateTime <= 0) return; currentTime++; setDirty(); @@ -737,7 +750,40 @@ public void onServerTick(ServerTickEvent.Pre event) this.unifiedStorage.insert(new ItemStackKey(stack), stack.getCount(), false); currentTime = 0; } + } + + private void tickPigBreeding() + { + long pigs = unifiedStorage.getStackByKey(PigStackKey.INSTANCE).amount(); + ItemStack carrot = new ItemStack(Items.CARROT); + ItemStackKey carrotKey = new ItemStackKey(carrot); + long carrots = unifiedStorage.getStackByKey(carrotKey).amount(); + + if (pigs < 2 || carrots < 2) + { + if (pigBreedingTime != 0) + { + pigBreedingTime = 0; + setDirty(); + } + return; + } + + pigBreedingTime++; + if (pigBreedingTime % 20 == 0) setDirty(); + if (pigBreedingTime < PIG_BREEDING_INTERVAL) return; + + long requestedBirths = Math.min(pigs / 2, carrots / 2); + long remainder = unifiedStorage.insert(PigStackKey.INSTANCE, requestedBirths, true).amount(); + long births = requestedBirths - remainder; + if (births <= 0) return; + + long carrotCost = births * 2; + KeyAmount extracted = unifiedStorage.extract(carrotKey, carrotCost, false, false); + if (extracted.amount() != carrotCost) return; + unifiedStorage.insert(PigStackKey.INSTANCE, births, false); + pigBreedingTime = 0; } private void syncPlayerMembership(UUID playerId, boolean switchPrimary) diff --git a/src/main/java/com/wintercogs/beyonddimensions/api/longtype/PigType.java b/src/main/java/com/wintercogs/beyonddimensions/api/longtype/PigType.java new file mode 100644 index 00000000..d1449d2a --- /dev/null +++ b/src/main/java/com/wintercogs/beyonddimensions/api/longtype/PigType.java @@ -0,0 +1,38 @@ +package com.wintercogs.beyonddimensions.api.longtype; + +import com.mojang.serialization.Codec; +import net.minecraft.network.chat.Component; + +public final class PigType extends LongType +{ + public static final Codec CODEC = createCodec(PigType::new); + + public PigType(long amount) + { + this.stackCount = amount; + } + + @Override + public Component getName() + { + return Component.translatable("types.beyonddimensions.pig.name"); + } + + @Override + public PigType getEmpty() + { + return new PigType(0); + } + + @Override + public PigType copy() + { + return new PigType(stackCount); + } + + @Override + public PigType copyWithAmount(long amount) + { + return new PigType(amount); + } +} diff --git a/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/impl/PigStackKey.java b/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/impl/PigStackKey.java new file mode 100644 index 00000000..e06fa712 --- /dev/null +++ b/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/impl/PigStackKey.java @@ -0,0 +1,147 @@ +package com.wintercogs.beyonddimensions.api.storage.key.impl; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.MapCodec; +import com.wintercogs.beyonddimensions.api.ids.BDConstants; +import com.wintercogs.beyonddimensions.api.longtype.PigType; +import com.wintercogs.beyonddimensions.api.storage.key.IStackRender; +import com.wintercogs.beyonddimensions.api.storage.key.KeyAmount; +import com.wintercogs.beyonddimensions.api.storage.key.render.PigStackKeyRender; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.stream.Stream; + +public final class PigStackKey extends LongStackKey +{ + public static final ResourceLocation ID = + ResourceLocation.fromNamespaceAndPath(BDConstants.MODID, "stack_type/pig"); + public static final PigStackKey INSTANCE = new PigStackKey(); + + public static final MapCodec TYPE_CODEC = new MapCodec<>() + { + @Override + public DataResult decode(com.mojang.serialization.DynamicOps ops, + com.mojang.serialization.MapLike input) + { + return DataResult.success(INSTANCE); + } + + @Override + public com.mojang.serialization.RecordBuilder encode(PigStackKey value, + com.mojang.serialization.DynamicOps ops, + com.mojang.serialization.RecordBuilder prefix) + { + return prefix; + } + + @Override + public Stream keys(com.mojang.serialization.DynamicOps ops) + { + return Stream.empty(); + } + }; + + public static final Codec CODEC = TYPE_CODEC.codec(); + + private PigStackKey() + { + this.stack = new PigType(0); + } + + @Override + public ResourceLocation getTypeID() + { + return ID; + } + + @Override + public MapCodec codec() + { + return TYPE_CODEC; + } + + @Override + public @Nullable KeyAmount fromStackObject(Object stack) + { + return stack instanceof PigType pig ? new KeyAmount(INSTANCE, pig.getStackCount()) : null; + } + + @Override + public @Nullable PigStackKey fromSourceObject(Object key, DataComponentPatch ignored) + { + return key instanceof PigType || key instanceof Number ? INSTANCE : null; + } + + @Override + public @NotNull PigType getSource() + { + return stack; + } + + @Override + public String getModId() + { + return "Minecraft"; + } + + @Override + public PigStackKey getEmpty() + { + return INSTANCE; + } + + @Override + public PigType getEmptyStack() + { + return new PigType(0); + } + + @Override + public boolean hasTag(TagKey tagKey) + { + return false; + } + + @Override + public Stream> getTags() + { + return Stream.empty(); + } + + @Override + public void serialize(RegistryFriendlyByteBuf buf) + { + } + + @Override + public @NotNull PigStackKey deserialize(RegistryFriendlyByteBuf buf) + { + return INSTANCE; + } + + @Override + public @NotNull CompoundTag serializeNBT(HolderLookup.Provider access) + { + return new CompoundTag(); + } + + @Override + public @NotNull PigStackKey deserializeNBT(CompoundTag nbt, HolderLookup.Provider access) + { + return INSTANCE; + } + + @Override + public @NotNull IStackRender getRender() + { + return PigStackKeyRender.INSTANCE; + } +} diff --git a/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/render/PigStackKeyRender.java b/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/render/PigStackKeyRender.java new file mode 100644 index 00000000..f067b405 --- /dev/null +++ b/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/render/PigStackKeyRender.java @@ -0,0 +1,94 @@ +package com.wintercogs.beyonddimensions.api.storage.key.render; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.wintercogs.beyonddimensions.api.storage.key.IStackKey; +import com.wintercogs.beyonddimensions.api.storage.key.IStackRender; +import com.wintercogs.beyonddimensions.api.storage.key.impl.PigStackKey; +import com.wintercogs.beyonddimensions.util.StringFormat; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.Items; +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.neoforged.neoforge.client.ClientTooltipFlag; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Optional; + +public final class PigStackKeyRender implements IStackRender +{ + public static final PigStackKeyRender INSTANCE = new PigStackKeyRender(); + + private PigStackKeyRender() + { + } + + @Override + public void render(GuiGraphics gui, IStackKey key, int x, int y) + { + gui.renderFakeItem(new ItemStack(Items.PIG_SPAWN_EGG), x, y); + } + + @Override + public void renderAmount(GuiGraphics gui, long amount, int x, int y) + { + String text = getCountText(amount); + if (text.isEmpty()) return; + + float scale = 0.666f; + var pose = gui.pose(); + pose.pushPose(); + pose.translate(0, 0, 200); + pose.scale(scale, scale, scale); + RenderSystem.disableBlend(); + int width = Minecraft.getInstance().font.width(text); + int renderX = (int) ((x - 1 + 18.0f - width * scale) / scale); + int renderY = (int) ((y - 1 + 16.0f - 5.0f * scale) / scale); + gui.drawString(Minecraft.getInstance().font, text, renderX, renderY, 0xFFFFFF); + pose.popPose(); + } + + @Override + public String getCountText(long count) + { + return count < 0 ? "" : StringFormat.formatCount(count); + } + + @Override + public Component getDisplayName(IStackKey key) + { + return PigStackKey.INSTANCE.getRenderStack().getName(); + } + + @Override + public List getTooltipLines(IStackKey key, long amount, Item.TooltipContext context, + @Nullable net.minecraft.world.entity.player.Player player, + TooltipFlag flag) + { + return List.of(getDisplayName(key), + Component.translatable("istack.beyonddimensions.storage_num.long_type", amount)); + } + + @Override + public Optional getTooltipImage(IStackKey key) + { + return Optional.empty(); + } + + @Override + public void renderTooltip(GuiGraphics gui, Font font, IStackKey key, long amount, int mouseX, int mouseY) + { + var minecraft = Minecraft.getInstance(); + var context = minecraft.level == null ? Item.TooltipContext.EMPTY : Item.TooltipContext.of(minecraft.level); + gui.renderTooltip(minecraft.font, + getTooltipLines(key, amount, context, minecraft.player, + ClientTooltipFlag.of(minecraft.options.advancedItemTooltips + ? TooltipFlag.Default.ADVANCED : TooltipFlag.Default.NORMAL)), + getTooltipImage(key), ItemStack.EMPTY, mouseX, mouseY); + } +} diff --git a/src/main/java/com/wintercogs/beyonddimensions/common/item/NetMagnetItem.java b/src/main/java/com/wintercogs/beyonddimensions/common/item/NetMagnetItem.java index ef3bfe0c..0105be0c 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/common/item/NetMagnetItem.java +++ b/src/main/java/com/wintercogs/beyonddimensions/common/item/NetMagnetItem.java @@ -5,6 +5,7 @@ import com.wintercogs.beyonddimensions.api.storage.key.KeyAmount; import com.wintercogs.beyonddimensions.api.storage.key.impl.FluidStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.ItemStackKey; +import com.wintercogs.beyonddimensions.api.storage.key.impl.PigStackKey; import com.wintercogs.beyonddimensions.common.init.BDDataComponents; import com.wintercogs.beyonddimensions.common.init.BDFluids; import com.wintercogs.beyonddimensions.common.machine.*; @@ -21,6 +22,7 @@ import net.minecraft.world.SimpleMenuProvider; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.animal.Pig; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -118,6 +120,16 @@ public void workContent(ItemStack stack, Level level, Entity holder, int slotId, UnifiedStorage storage = NetedItem.getNet(stack).getUnifiedStorage(); + // 将范围内的猪收进维度网络。先模拟插入,成功后才移除实体。 + for (Pig pig : level.getEntitiesOfClass(Pig.class, searchArea, Entity::isAlive)) + { + if (storage.insert(PigStackKey.INSTANCE, 1, true).isEmpty()) + { + storage.insert(PigStackKey.INSTANCE, 1, false); + pig.discard(); + } + } + // 开始收集物品 if (hopperItemMode == HopperItemMode.ALLOW) { diff --git a/src/main/resources/assets/beyonddimensions/lang/en_us.json b/src/main/resources/assets/beyonddimensions/lang/en_us.json index 8a503c0f..4859f3f5 100644 --- a/src/main/resources/assets/beyonddimensions/lang/en_us.json +++ b/src/main/resources/assets/beyonddimensions/lang/en_us.json @@ -224,5 +224,6 @@ "msg.beyonddimensions.primary_net.switch.cleared": "Primary network cleared", "msg.beyonddimensions.primary_net.switch.none_available": "No available network to switch to", "msg.beyonddimensions.primary_net.switch.invalid_target": "That network is unavailable or you are no longer a member", - "msg.beyonddimensions.primary_net.switch.no_other": "No other primary network is available" + "msg.beyonddimensions.primary_net.switch.no_other": "No other primary network is available", + "types.beyonddimensions.pig.name": "Pigs" } diff --git a/src/main/resources/assets/beyonddimensions/lang/zh_cn.json b/src/main/resources/assets/beyonddimensions/lang/zh_cn.json index 208b6f55..9ea199fc 100644 --- a/src/main/resources/assets/beyonddimensions/lang/zh_cn.json +++ b/src/main/resources/assets/beyonddimensions/lang/zh_cn.json @@ -224,5 +224,6 @@ "msg.beyonddimensions.primary_net.switch.cleared": "已切换为无主网络", "msg.beyonddimensions.primary_net.switch.none_available": "当前没有可切换的网络", "msg.beyonddimensions.primary_net.switch.invalid_target": "目标网络不可用,或你已经不再属于该网络", - "msg.beyonddimensions.primary_net.switch.no_other": "没有其他可切换的主网络" + "msg.beyonddimensions.primary_net.switch.no_other": "没有其他可切换的主网络", + "types.beyonddimensions.pig.name": "猪" } From f87d339b48f7c98922f3ebbb781167915c6911c5 Mon Sep 17 00:00:00 2001 From: frostbite-bot <304862314+frostbite-bot@users.noreply.github.com> Date: Wed, 29 Jul 2026 01:57:05 +0000 Subject: [PATCH 2/2] Add universal dimensional ranch breeding --- .../beyonddimensions/BeyondDimensions.java | 4 +- .../api/dimensionnet/DimensionsNet.java | 221 +++++++++++++++--- .../api/longtype/MobType.java | 52 +++++ .../api/storage/key/impl/MobStackKey.java | 173 ++++++++++++++ .../storage/key/render/MobStackKeyRender.java | 105 +++++++++ .../common/item/NetMagnetItem.java | 36 ++- 6 files changed, 555 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/wintercogs/beyonddimensions/api/longtype/MobType.java create mode 100644 src/main/java/com/wintercogs/beyonddimensions/api/storage/key/impl/MobStackKey.java create mode 100644 src/main/java/com/wintercogs/beyonddimensions/api/storage/key/render/MobStackKeyRender.java diff --git a/src/main/java/com/wintercogs/beyonddimensions/BeyondDimensions.java b/src/main/java/com/wintercogs/beyonddimensions/BeyondDimensions.java index 3d0f6ad6..92dfd11a 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/BeyondDimensions.java +++ b/src/main/java/com/wintercogs/beyonddimensions/BeyondDimensions.java @@ -18,6 +18,7 @@ import com.wintercogs.beyonddimensions.api.storage.key.impl.EnergyStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.FluidStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.ItemStackKey; +import com.wintercogs.beyonddimensions.api.storage.key.impl.MobStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.PigStackKey; import com.wintercogs.beyonddimensions.common.init.*; import com.wintercogs.beyonddimensions.integration.IntegrationManager; @@ -67,7 +68,8 @@ private void commonSetup(final FMLCommonSetupEvent event) StackKeyRegistry.registerType(ItemStackKey.EMPTY); StackKeyRegistry.registerType(FluidStackKey.EMPTY); StackKeyRegistry.registerType(EnergyStackKey.INSTANCE); - StackKeyRegistry.registerType(PigStackKey.INSTANCE); + StackKeyRegistry.registerType(PigStackKey.INSTANCE); // 兼容旧版维度养猪场存档 + StackKeyRegistry.registerType(MobStackKey.EMPTY); // 注册方块能力类型,用于动态为方块注册能力 CapabilityHelper.BlockCapabilityMap.put(ItemStackKey.ID, Capabilities.ItemHandler.BLOCK); diff --git a/src/main/java/com/wintercogs/beyonddimensions/api/dimensionnet/DimensionsNet.java b/src/main/java/com/wintercogs/beyonddimensions/api/dimensionnet/DimensionsNet.java index 55da4cba..7e3ed1cd 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/api/dimensionnet/DimensionsNet.java +++ b/src/main/java/com/wintercogs/beyonddimensions/api/dimensionnet/DimensionsNet.java @@ -5,16 +5,24 @@ import com.wintercogs.beyonddimensions.api.storage.key.KeyAmount; import com.wintercogs.beyonddimensions.api.storage.key.impl.EnergyStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.ItemStackKey; +import com.wintercogs.beyonddimensions.api.storage.key.impl.MobStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.PigStackKey; import com.wintercogs.beyonddimensions.common.init.BDItems; import com.wintercogs.beyonddimensions.config.ServerConfigRuntime; import com.wintercogs.beyonddimensions.util.PlayerNameHelper; import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.StringTag; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.npc.Villager; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; @@ -96,11 +104,19 @@ public class DimensionsNet extends SavedData private int currentTime = 0; /** - * 维度养猪场繁殖进度。正常猪被喂食后约需5分钟才能再次繁殖, - * 因此网络每6000 tick消耗两根胡萝卜并增加一只猪。 + * 各类动物和村民的繁殖进度。每种实体独立计时,约5分钟完成一轮繁殖。 */ - private int pigBreedingTime = 0; - private static final int PIG_BREEDING_INTERVAL = 6000; + private final Map mobBreedingTimes = new HashMap<>(); + private final Map, ItemStackKey> animalFoodKeys = new HashMap<>(); + private static final int MOB_BREEDING_INTERVAL = 6000; + private static final int RANCH_TICK_INTERVAL = 20; + private static final int VILLAGER_FOOD_POINTS = 12; + private static final Map VILLAGER_FOODS = Map.of( + Items.BREAD, 4, + Items.CARROT, 1, + Items.POTATO, 1, + Items.BEETROOT, 1 + ); /** * 构造函数 @@ -365,6 +381,13 @@ public static DimensionsNet load(CompoundTag tag, HolderLookup.Provider registry } net.unifiedStorage.deserializeNBT(registryAccess, tag.getCompound("UnifiedStorage")); + // 把旧版养猪场的猪数量迁移到通用实体存储。 + long legacyPigs = net.unifiedStorage.getStackByKey(PigStackKey.INSTANCE).amount(); + if (legacyPigs > 0) + { + net.unifiedStorage.extract(PigStackKey.INSTANCE, legacyPigs, false, false); + net.unifiedStorage.insert(new MobStackKey(EntityType.PIG), legacyPigs, false); + } // 旧数据兼容 if (tag.contains("EnergyStorage")) { @@ -389,7 +412,16 @@ public static DimensionsNet load(CompoundTag tag, HolderLookup.Provider registry // 读取倒计时 net.currentTime = tag.getInt("currentTime"); - net.pigBreedingTime = tag.getInt("pigBreedingTime"); + CompoundTag breedingTimesTag = tag.getCompound("mobBreedingTimes"); + for (String key : breedingTimesTag.getAllKeys()) + { + ResourceLocation typeId = ResourceLocation.tryParse(key); + if (typeId != null) + net.mobBreedingTimes.put(typeId, Math.max(0, breedingTimesTag.getInt(key))); + } + if (tag.contains("pigBreedingTime")) + net.mobBreedingTimes.put(BuiltInRegistries.ENTITY_TYPE.getKey(EntityType.PIG), + Math.max(0, tag.getInt("pigBreedingTime"))); if (tag.contains("Deleted")) net.deleted = tag.getBoolean("Deleted"); @@ -438,7 +470,9 @@ public static DimensionsNet load(CompoundTag tag, HolderLookup.Provider registry // 保存倒计时 tag.putInt("currentTime", this.currentTime); - tag.putInt("pigBreedingTime", this.pigBreedingTime); + CompoundTag breedingTimesTag = new CompoundTag(); + mobBreedingTimes.forEach((typeId, time) -> breedingTimesTag.putInt(typeId.toString(), time)); + tag.put("mobBreedingTimes", breedingTimesTag); // 保存删除状态 tag.putBoolean("Deleted", this.deleted); @@ -738,7 +772,7 @@ public void onServerTick(ServerTickEvent.Pre event) { if (temporary) return; - tickPigBreeding(); + tickRanchBreeding(event.getServer().overworld()); if (ServerConfigRuntime.crystalGenerateTime <= 0) return; @@ -752,38 +786,169 @@ public void onServerTick(ServerTickEvent.Pre event) } } - private void tickPigBreeding() + private void tickRanchBreeding(ServerLevel level) { - long pigs = unifiedStorage.getStackByKey(PigStackKey.INSTANCE).amount(); - ItemStack carrot = new ItemStack(Items.CARROT); - ItemStackKey carrotKey = new ItemStackKey(carrot); - long carrots = unifiedStorage.getStackByKey(carrotKey).amount(); + if (level.getGameTime() % RANCH_TICK_INTERVAL != 0) return; - if (pigs < 2 || carrots < 2) + Set activeTypes = new HashSet<>(); + for (KeyAmount entry : List.copyOf(unifiedStorage.getStorage())) { - if (pigBreedingTime != 0) + if (!(entry.key() instanceof MobStackKey mobKey) || entry.amount() < 2) continue; + + ResourceLocation typeId = BuiltInRegistries.ENTITY_TYPE.getKey(mobKey.entityType()); + activeTypes.add(typeId); + if (!hasBreedingFood(level, mobKey.entityType())) + { + resetBreedingTime(typeId); + continue; + } + + int breedingTime = mobBreedingTimes.getOrDefault(typeId, 0) + RANCH_TICK_INTERVAL; + mobBreedingTimes.put(typeId, breedingTime); + setDirty(); + if (breedingTime < MOB_BREEDING_INTERVAL) continue; + + if (breedStoredMobs(level, mobKey, entry.amount())) { - pigBreedingTime = 0; + mobBreedingTimes.put(typeId, 0); setDirty(); } - return; } - pigBreedingTime++; - if (pigBreedingTime % 20 == 0) setDirty(); - if (pigBreedingTime < PIG_BREEDING_INTERVAL) return; + if (mobBreedingTimes.keySet().removeIf(typeId -> !activeTypes.contains(typeId))) + setDirty(); + } + + private boolean hasBreedingFood(ServerLevel level, EntityType entityType) + { + if (entityType == EntityType.VILLAGER) + return getVillagerFoodPoints() >= VILLAGER_FOOD_POINTS; + + ItemStackKey cachedFood = animalFoodKeys.get(entityType); + if (cachedFood != null) + { + if (unifiedStorage.getStackByKey(cachedFood).amount() > 0) return true; + animalFoodKeys.remove(entityType); + } + + Entity entity = entityType.create(level); + if (!(entity instanceof Animal animal)) return false; + try + { + ItemStackKey foodKey = findAnimalFood(animal); + if (foodKey == null) return false; + animalFoodKeys.put(entityType, foodKey); + return true; + } + finally + { + entity.discard(); + } + } + + private boolean breedStoredMobs(ServerLevel level, MobStackKey mobKey, long mobCount) + { + if (mobKey.entityType() == EntityType.VILLAGER) + return breedVillagers(mobKey, mobCount); - long requestedBirths = Math.min(pigs / 2, carrots / 2); - long remainder = unifiedStorage.insert(PigStackKey.INSTANCE, requestedBirths, true).amount(); - long births = requestedBirths - remainder; - if (births <= 0) return; + ItemStackKey foodKey = animalFoodKeys.get(mobKey.entityType()); + if (foodKey == null) return false; - long carrotCost = births * 2; - KeyAmount extracted = unifiedStorage.extract(carrotKey, carrotCost, false, false); - if (extracted.amount() != carrotCost) return; + long foodCount = unifiedStorage.getStackByKey(foodKey).amount(); + long births = Math.min(mobCount / 2, foodCount / 2); + births = Math.min(births, Long.MAX_VALUE / 2); + return completeBreeding(mobKey, births, Map.of(foodKey, births * 2)); + } - unifiedStorage.insert(PigStackKey.INSTANCE, births, false); - pigBreedingTime = 0; + private @Nullable ItemStackKey findAnimalFood(Animal animal) + { + for (KeyAmount entry : List.copyOf(unifiedStorage.getStorage())) + { + if (entry.amount() > 0 && entry.key() instanceof ItemStackKey itemKey + && animal.isFood(itemKey.copyStack())) + return itemKey; + } + return null; + } + + private boolean breedVillagers(MobStackKey mobKey, long mobCount) + { + long births = Math.min(mobCount / 2, getVillagerFoodPoints() / VILLAGER_FOOD_POINTS); + births = Math.min(births, Long.MAX_VALUE / VILLAGER_FOOD_POINTS); + births -= unifiedStorage.insert(mobKey, births, true).amount(); + if (births <= 0) return false; + + long requiredPoints = births * VILLAGER_FOOD_POINTS; + Map costs = new LinkedHashMap<>(); + for (Map.Entry food : VILLAGER_FOODS.entrySet()) + { + ItemStackKey foodKey = new ItemStackKey(new ItemStack(food.getKey())); + long available = unifiedStorage.getStackByKey(foodKey).amount(); + long itemsNeeded = (requiredPoints + food.getValue() - 1) / food.getValue(); + long used = Math.min(available, itemsNeeded); + if (used <= 0) continue; + + costs.put(foodKey, used); + requiredPoints = Math.max(0, requiredPoints - used * food.getValue()); + if (requiredPoints == 0) break; + } + return requiredPoints == 0 && completeBreeding(mobKey, births, costs); + } + + private long getVillagerFoodPoints() + { + long points = 0; + for (Map.Entry food : VILLAGER_FOODS.entrySet()) + { + long count = unifiedStorage.getStackByKey(new ItemStackKey(new ItemStack(food.getKey()))).amount(); + if (count > (Long.MAX_VALUE - points) / food.getValue()) return Long.MAX_VALUE; + points += count * food.getValue(); + } + return points; + } + + private boolean completeBreeding(MobStackKey mobKey, long births, Map costs) + { + if (births <= 0) return false; + + long accepted = births - unifiedStorage.insert(mobKey, births, true).amount(); + if (accepted <= 0) return false; + if (accepted != births) + { + births = accepted; + if (costs.size() != 1) return false; + + ItemStackKey key = costs.keySet().iterator().next(); + costs = Map.of(key, births * 2); + } + + Map extractedFoods = new LinkedHashMap<>(); + for (Map.Entry cost : costs.entrySet()) + { + KeyAmount extracted = unifiedStorage.extract(cost.getKey(), cost.getValue(), false, false); + if (extracted.amount() != cost.getValue()) + { + extractedFoods.forEach((key, amount) -> unifiedStorage.insert(key, amount, false)); + if (extracted.amount() > 0) unifiedStorage.insert(cost.getKey(), extracted.amount(), false); + return false; + } + extractedFoods.put(cost.getKey(), extracted.amount()); + } + + long inserted = births - unifiedStorage.insert(mobKey, births, false).amount(); + if (inserted != births) + { + if (inserted > 0) unifiedStorage.extract(mobKey, inserted, false, false); + extractedFoods.forEach((key, amount) -> unifiedStorage.insert(key, amount, false)); + return false; + } + return true; + } + + private void resetBreedingTime(ResourceLocation typeId) + { + if (mobBreedingTimes.remove(typeId) != null) + setDirty(); } private void syncPlayerMembership(UUID playerId, boolean switchPrimary) diff --git a/src/main/java/com/wintercogs/beyonddimensions/api/longtype/MobType.java b/src/main/java/com/wintercogs/beyonddimensions/api/longtype/MobType.java new file mode 100644 index 00000000..daf8bd60 --- /dev/null +++ b/src/main/java/com/wintercogs/beyonddimensions/api/longtype/MobType.java @@ -0,0 +1,52 @@ +package com.wintercogs.beyonddimensions.api.longtype; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.EntityType; + +public final class MobType extends LongType +{ + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + BuiltInRegistries.ENTITY_TYPE.byNameCodec().fieldOf("entity_type").forGetter(MobType::entityType), + Codec.LONG.fieldOf("amount").forGetter(MobType::getStackCount) + ).apply(instance, MobType::new)); + + private final EntityType entityType; + + public MobType(EntityType entityType, long amount) + { + this.entityType = entityType; + this.stackCount = amount; + } + + public EntityType entityType() + { + return entityType; + } + + @Override + public Component getName() + { + return entityType.getDescription(); + } + + @Override + public MobType getEmpty() + { + return new MobType(entityType, 0); + } + + @Override + public MobType copy() + { + return new MobType(entityType, stackCount); + } + + @Override + public MobType copyWithAmount(long amount) + { + return new MobType(entityType, amount); + } +} diff --git a/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/impl/MobStackKey.java b/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/impl/MobStackKey.java new file mode 100644 index 00000000..96f94fd9 --- /dev/null +++ b/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/impl/MobStackKey.java @@ -0,0 +1,173 @@ +package com.wintercogs.beyonddimensions.api.storage.key.impl; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.wintercogs.beyonddimensions.api.ids.BDConstants; +import com.wintercogs.beyonddimensions.api.longtype.MobType; +import com.wintercogs.beyonddimensions.api.storage.key.IStackRender; +import com.wintercogs.beyonddimensions.api.storage.key.KeyAmount; +import com.wintercogs.beyonddimensions.api.storage.key.render.MobStackKeyRender; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.EntityType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.stream.Stream; + +public final class MobStackKey extends LongStackKey +{ + public static final ResourceLocation ID = + ResourceLocation.fromNamespaceAndPath(BDConstants.MODID, "stack_type/mob"); + public static final MobStackKey EMPTY = new MobStackKey(EntityType.PIG); + + public static final MapCodec TYPE_CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + BuiltInRegistries.ENTITY_TYPE.byNameCodec().fieldOf("entity_type").forGetter(MobStackKey::entityType) + ).apply(instance, MobStackKey::new)); + + private final EntityType entityType; + + public MobStackKey(EntityType entityType) + { + this.entityType = entityType; + this.stack = new MobType(entityType, 0); + } + + public EntityType entityType() + { + return entityType; + } + + @Override + public ResourceLocation getTypeID() + { + return ID; + } + + @Override + public MapCodec codec() + { + return TYPE_CODEC; + } + + @Override + public @Nullable KeyAmount fromStackObject(Object stack) + { + return stack instanceof MobType mob + ? new KeyAmount(new MobStackKey(mob.entityType()), mob.getStackCount()) + : null; + } + + @Override + public @Nullable MobStackKey fromSourceObject(Object key, DataComponentPatch ignored) + { + if (key instanceof EntityType type) return new MobStackKey(type); + if (key instanceof MobType mob) return new MobStackKey(mob.entityType()); + return null; + } + + @Override + public @NotNull MobType getSource() + { + return stack; + } + + @Override + public String getModId() + { + ResourceLocation id = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + return id == null ? "Minecraft" : id.getNamespace(); + } + + @Override + public MobStackKey getEmpty() + { + return EMPTY; + } + + @Override + public MobType getEmptyStack() + { + return new MobType(entityType, 0); + } + + @Override + public boolean hasTag(TagKey tagKey) + { + return false; + } + + @Override + public Stream> getTags() + { + return Stream.empty(); + } + + @Override + public boolean isSame(com.wintercogs.beyonddimensions.api.storage.key.IStackKey other) + { + return other instanceof MobStackKey mob && mob.entityType == entityType; + } + + @Override + public boolean isSameTypeSameComponents(com.wintercogs.beyonddimensions.api.storage.key.IStackKey other) + { + return isSame(other); + } + + @Override + public boolean equals(Object other) + { + return other instanceof MobStackKey mob && mob.entityType == entityType; + } + + @Override + public int hashCode() + { + return Objects.hash(ID, entityType); + } + + @Override + public void serialize(RegistryFriendlyByteBuf buf) + { + ByteBufCodecs.registry(net.minecraft.core.registries.Registries.ENTITY_TYPE).encode(buf, entityType); + } + + @Override + public @NotNull MobStackKey deserialize(RegistryFriendlyByteBuf buf) + { + return new MobStackKey(ByteBufCodecs.registry(net.minecraft.core.registries.Registries.ENTITY_TYPE).decode(buf)); + } + + @Override + public @NotNull CompoundTag serializeNBT(HolderLookup.Provider access) + { + CompoundTag tag = new CompoundTag(); + ResourceLocation typeId = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + tag.putString("entity_type", typeId.toString()); + return tag; + } + + @Override + public @NotNull MobStackKey deserializeNBT(CompoundTag nbt, HolderLookup.Provider access) + { + ResourceLocation typeId = ResourceLocation.tryParse(nbt.getString("entity_type")); + EntityType type = typeId == null + ? EntityType.PIG + : BuiltInRegistries.ENTITY_TYPE.getOptional(typeId).orElse(EntityType.PIG); + return new MobStackKey(type); + } + + @Override + public @NotNull IStackRender getRender() + { + return MobStackKeyRender.INSTANCE; + } +} diff --git a/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/render/MobStackKeyRender.java b/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/render/MobStackKeyRender.java new file mode 100644 index 00000000..2a21766e --- /dev/null +++ b/src/main/java/com/wintercogs/beyonddimensions/api/storage/key/render/MobStackKeyRender.java @@ -0,0 +1,105 @@ +package com.wintercogs.beyonddimensions.api.storage.key.render; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.wintercogs.beyonddimensions.api.storage.key.IStackKey; +import com.wintercogs.beyonddimensions.api.storage.key.IStackRender; +import com.wintercogs.beyonddimensions.api.storage.key.impl.MobStackKey; +import com.wintercogs.beyonddimensions.util.StringFormat; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.item.TooltipFlag; +import net.neoforged.neoforge.client.ClientTooltipFlag; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Optional; + +public final class MobStackKeyRender implements IStackRender +{ + public static final MobStackKeyRender INSTANCE = new MobStackKeyRender(); + + private MobStackKeyRender() + { + } + + private ItemStack icon(IStackKey key) + { + if (key instanceof MobStackKey mob) + { + SpawnEggItem egg = SpawnEggItem.byId(mob.entityType()); + if (egg != null) return new ItemStack(egg); + } + return new ItemStack(Items.LEAD); + } + + @Override + public void render(GuiGraphics gui, IStackKey key, int x, int y) + { + gui.renderFakeItem(icon(key), x, y); + } + + @Override + public void renderAmount(GuiGraphics gui, long amount, int x, int y) + { + String text = getCountText(amount); + if (text.isEmpty()) return; + + float scale = 0.666f; + var pose = gui.pose(); + pose.pushPose(); + pose.translate(0, 0, 200); + pose.scale(scale, scale, scale); + RenderSystem.disableBlend(); + int width = Minecraft.getInstance().font.width(text); + int renderX = (int) ((x - 1 + 18.0f - width * scale) / scale); + int renderY = (int) ((y - 1 + 16.0f - 5.0f * scale) / scale); + gui.drawString(Minecraft.getInstance().font, text, renderX, renderY, 0xFFFFFF); + pose.popPose(); + } + + @Override + public String getCountText(long count) + { + return count < 0 ? "" : StringFormat.formatCount(count); + } + + @Override + public Component getDisplayName(IStackKey key) + { + return key instanceof MobStackKey mob ? mob.entityType().getDescription() : Component.empty(); + } + + @Override + public List getTooltipLines(IStackKey key, long amount, Item.TooltipContext context, + @Nullable net.minecraft.world.entity.player.Player player, + TooltipFlag flag) + { + return List.of(getDisplayName(key), + Component.translatable("istack.beyonddimensions.storage_num.long_type", amount)); + } + + @Override + public Optional getTooltipImage(IStackKey key) + { + return Optional.empty(); + } + + @Override + public void renderTooltip(GuiGraphics gui, Font font, IStackKey key, long amount, int mouseX, int mouseY) + { + var minecraft = Minecraft.getInstance(); + var context = minecraft.level == null ? Item.TooltipContext.EMPTY : Item.TooltipContext.of(minecraft.level); + gui.renderTooltip(minecraft.font, + getTooltipLines(key, amount, context, minecraft.player, + ClientTooltipFlag.of(minecraft.options.advancedItemTooltips + ? TooltipFlag.Default.ADVANCED : TooltipFlag.Default.NORMAL)), + getTooltipImage(key), icon(key), mouseX, mouseY); + } +} diff --git a/src/main/java/com/wintercogs/beyonddimensions/common/item/NetMagnetItem.java b/src/main/java/com/wintercogs/beyonddimensions/common/item/NetMagnetItem.java index 0105be0c..5c474c1d 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/common/item/NetMagnetItem.java +++ b/src/main/java/com/wintercogs/beyonddimensions/common/item/NetMagnetItem.java @@ -5,7 +5,7 @@ import com.wintercogs.beyonddimensions.api.storage.key.KeyAmount; import com.wintercogs.beyonddimensions.api.storage.key.impl.FluidStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.ItemStackKey; -import com.wintercogs.beyonddimensions.api.storage.key.impl.PigStackKey; +import com.wintercogs.beyonddimensions.api.storage.key.impl.MobStackKey; import com.wintercogs.beyonddimensions.common.init.BDDataComponents; import com.wintercogs.beyonddimensions.common.init.BDFluids; import com.wintercogs.beyonddimensions.common.machine.*; @@ -20,9 +20,14 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.AgeableMob; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ExperienceOrb; -import net.minecraft.world.entity.animal.Pig; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.Leashable; +import net.minecraft.world.entity.TamableAnimal; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerProfession; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -120,13 +125,14 @@ public void workContent(ItemStack stack, Level level, Entity holder, int slotId, UnifiedStorage storage = NetedItem.getNet(stack).getUnifiedStorage(); - // 将范围内的猪收进维度网络。先模拟插入,成功后才移除实体。 - for (Pig pig : level.getEntitiesOfClass(Pig.class, searchArea, Entity::isAlive)) + // 收集可繁殖动物和村民。只接收成年、无自定义数据且未骑乘的实体。 + for (AgeableMob mob : level.getEntitiesOfClass(AgeableMob.class, searchArea, this::canCollectMob)) { - if (storage.insert(PigStackKey.INSTANCE, 1, true).isEmpty()) + MobStackKey mobKey = new MobStackKey(mob.getType()); + if (storage.insert(mobKey, 1, true).isEmpty()) { - storage.insert(PigStackKey.INSTANCE, 1, false); - pig.discard(); + storage.insert(mobKey, 1, false); + mob.discard(); } } @@ -193,6 +199,22 @@ public void workContent(ItemStack stack, Level level, Entity holder, int slotId, } } + private boolean canCollectMob(AgeableMob mob) + { + return mob.isAlive() + && !mob.isBaby() + && !mob.hasCustomName() + && !mob.isPassenger() + && !mob.isVehicle() + && (!(mob instanceof Leashable leashable) || !leashable.isLeashed()) + && (!(mob instanceof TamableAnimal tamable) || !tamable.isTame()) + && (!(mob instanceof Villager villager) + || villager.getVillagerXp() == 0 + && villager.getVillagerData().getProfession() == VillagerProfession.NONE + && villager.getInventory().isEmpty()) + && (mob instanceof Animal || mob instanceof Villager); + } + @Override public int getTicksPerWork(ItemStack stack, Level level, Entity holder, int slotId, boolean isSelected) {