From b6c3ac56f32be3ee429a306e7e5896b8fbe43c27 Mon Sep 17 00:00:00 2001 From: Yansunsky <1321833118@qq.com> Date: Wed, 10 Jun 2026 17:30:42 +0800 Subject: [PATCH 1/2] =?UTF-8?q?1.=E9=80=9A=E8=BF=87IO=E5=B9=B3=E8=A1=A1?= =?UTF-8?q?=E7=9A=84=E6=96=B9=E5=BC=8F=E9=A2=84=E9=98=B2=E7=8E=A9=E5=AE=B6?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E6=8F=90=E5=89=8D=E6=94=BE=E7=BD=AE=E5=A4=A7?= =?UTF-8?q?=E5=AE=B9=E9=87=8F=E5=AE=B9=E5=99=A8=E6=AC=BA=E9=AA=97=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=E3=80=82=202.=E5=90=AF=E5=8A=A8=E6=A3=92=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E7=94=A8=E4=BA=8E=E8=BF=98=E5=8E=9F=E5=B7=A5=E5=8E=82?= =?UTF-8?q?=203.=E6=B7=BB=E5=8A=A0=E9=BB=91=E5=90=8D=E5=8D=95=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=EF=BC=8C=E9=BB=91=E5=90=8D=E5=8D=95=E6=96=B9=E5=9D=97?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E8=AF=84=E4=BC=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/compactmachinespor/Config.java | 15 + .../block/BaseIOBlockEntity.java | 11 +- .../block/EvaluatorBlockEntity.java | 122 ++++++- .../block/FactoryBlock.java | 30 ++ .../block/FactoryBlockEntity.java | 18 + .../com/compactmachinespor/core/Core.java | 342 +++++++++++++++++- .../com/compactmachinespor/core/Machine.java | 39 ++ .../mixin/BoundCompactMachineBlockMixin.java | 14 +- .../assets/compactmachinespor/lang/en_us.json | 6 +- .../assets/compactmachinespor/lang/zh_cn.json | 6 +- 10 files changed, 591 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/compactmachinespor/Config.java b/src/main/java/com/compactmachinespor/Config.java index 3406c0c..92cc746 100644 --- a/src/main/java/com/compactmachinespor/Config.java +++ b/src/main/java/com/compactmachinespor/Config.java @@ -25,5 +25,20 @@ public class Config { .translation("config.compactmachinespor.unpack_permission_level") .defineInRange("PermissionLevel", 2, 0, 4); + public static final ModConfigSpec.BooleanValue ENABLE_INVENTORY_AUDIT = BUILDER + .comment("Enable inventory baseline audit (conservation check)") + .translation("config.compactmachinespor.enable_inventory_audit") + .define("EnableInventoryAudit", true); + + public static final ModConfigSpec.ConfigValue> SUSPICIOUS_MODS = BUILDER + .comment("Mod IDs whose blocks are untrusted (unscannable storage, e.g. AE2). Evaluation aborts if found.") + .translation("config.compactmachinespor.suspicious_mods") + .defineListAllowEmpty("SuspiciousMods", java.util.List.of("ae2", "refinedstorage"), o -> o instanceof String); + + public static final ModConfigSpec.ConfigValue> SUSPICIOUS_BLOCKS = BUILDER + .comment("Specific block IDs (namespace:path) that are untrusted.") + .translation("config.compactmachinespor.suspicious_blocks") + .defineListAllowEmpty("SuspiciousBlocks", java.util.List.of(), o -> o instanceof String); + static final ModConfigSpec SPEC = BUILDER.build(); } diff --git a/src/main/java/com/compactmachinespor/block/BaseIOBlockEntity.java b/src/main/java/com/compactmachinespor/block/BaseIOBlockEntity.java index 8da81e9..30caed6 100644 --- a/src/main/java/com/compactmachinespor/block/BaseIOBlockEntity.java +++ b/src/main/java/com/compactmachinespor/block/BaseIOBlockEntity.java @@ -133,14 +133,21 @@ protected DataSetType getDataSetType() { protected void handle(Holder holder, int count) { if (!checkAndDeactivate()) return; if (getLevel() instanceof ServerLevel serverLevel) { - Core.setMachineData(roomCode, getDataSetType(), holder, count, Core.getTicks(serverLevel)); + DataSetType type = getDataSetType(); + long ticks = Core.getTicks(serverLevel); + Core.setMachineData(roomCode, type, holder, count, ticks); + // 累计 IO 总量(用于库存基线审计) + Core.getMachine(roomCode).addTotal(type, holder, count); } } protected void handle(int energy) { if (!checkAndDeactivate()) return; if (getLevel() instanceof ServerLevel serverLevel) { - Core.getMachine(roomCode).addEnergyData(getDataSetType(), energy, Core.getTicks(serverLevel)); + DataSetType type = getDataSetType(); + Core.getMachine(roomCode).addEnergyData(type, energy, Core.getTicks(serverLevel)); + // 累计 IO 总量(用于库存基线审计) + Core.getMachine(roomCode).addTotalEnergy(type, energy); } } diff --git a/src/main/java/com/compactmachinespor/block/EvaluatorBlockEntity.java b/src/main/java/com/compactmachinespor/block/EvaluatorBlockEntity.java index d283d45..ff5e5a3 100644 --- a/src/main/java/com/compactmachinespor/block/EvaluatorBlockEntity.java +++ b/src/main/java/com/compactmachinespor/block/EvaluatorBlockEntity.java @@ -2,18 +2,138 @@ import com.compactmachinespor.Cyumocompactmachinespor; import com.compactmachinespor.core.Core; +import com.compactmachinespor.core.Core.CreateResult; import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; public class EvaluatorBlockEntity extends RoomCodeBlockEntity { + // 原机器方块信息,用于评估失败时还原 + private ResourceLocation originalMachineBlock; + private CompoundTag savedOriginalNbt; // 原 BE 的完整 NBT(含 data components) + public EvaluatorBlockEntity(BlockPos pos, BlockState blockState) { super(Cyumocompactmachinespor.EVALUATOR_BLOCK_ENTITY.get(), pos, blockState); } + public void saveOriginalMachine(ResourceLocation blockId, CompoundTag beNbt) { + this.originalMachineBlock = blockId; + this.savedOriginalNbt = beNbt; + setChanged(); + } + + /** + * 供 Core.createMachine 获取原始机器 NBT(用于提取附件数据) + */ + public CompoundTag getSavedOriginalNbt() { + return savedOriginalNbt; + } + public void trigger() { if (roomCode != null && !roomCode.isEmpty() && getLevel() instanceof ServerLevel serverLevel) { - Core.createMachine(serverLevel, roomCode, getBlockPos()); + CreateResult result = Core.createMachine(serverLevel, roomCode, getBlockPos()); + if (result == CreateResult.ABORTED_SUSPICIOUS_BLOCKS) { + serverLevel.players().forEach(p -> + p.displayClientMessage( + Component.translatable("chat.compactmachinespor.suspicious_blocks"), + false + ) + ); + restoreOriginalMachine(serverLevel); + } + } + } + + /** + * 还原为原来的紧缩空间机器方块 + * 延后到 neighborChanged 执行完毕后再执行,避免冲突 + */ + private void restoreOriginalMachine(ServerLevel level) { + if (originalMachineBlock == null || savedOriginalNbt == null) return; + + final BlockPos pos = getBlockPos(); + final Block machineBlock = BuiltInRegistries.BLOCK.get(originalMachineBlock); + final CompoundTag nbt = savedOriginalNbt.copy(); + if (machineBlock == null || machineBlock == net.minecraft.world.level.block.Blocks.AIR) return; + + Cyumocompactmachinespor.LOGGER.info("Will restore original machine at {} (delayed)", pos); + + // 使用 server.execute 在邻居更新完成后执行还原 + // 避免与 EvaluatorBlock.neighborChanged 中的 setBlockAndUpdate 冲突 + level.getServer().execute(() -> { + if (!(level.getBlockEntity(pos) instanceof EvaluatorBlockEntity)) { + return; + } + + // 1. 替换方块(会创建默认 BE) + level.removeBlockEntity(pos); + level.setBlockAndUpdate(pos, machineBlock.defaultBlockState()); + + // 2. 用 loadStatic 从保存的 NBT 创建完整的 BE(包含 neoforge:attachments) + // loadWithComponents 在已有 BE 上调用可能不覆盖全部数据, + // loadStatic 直接从 NBT 创建全新 BE,能正确恢复所有数据 + BlockEntity loaded = BlockEntity.loadStatic(pos, level.getBlockState(pos), nbt, level.registryAccess()); + if (loaded != null) { + level.setBlockEntity(loaded); + loaded.setChanged(); + Cyumocompactmachinespor.LOGGER.info("Machine restored at {} (via loadStatic)", pos); + } else { + Cyumocompactmachinespor.LOGGER.warn("loadStatic returned null at {}", pos); + } + + level.sendBlockUpdated(pos, level.getBlockState(pos), level.getBlockState(pos), Block.UPDATE_ALL); + }); + } + + @Override + protected void loadCommon(CompoundTag tag) { + super.loadCommon(tag); + this.originalMachineBlock = null; + this.savedOriginalNbt = null; + if (tag.contains("original_machine_block")) { + String id = tag.getString("original_machine_block"); + this.originalMachineBlock = ResourceLocation.tryParse(id); + } + if (tag.contains("original_machine_nbt")) { + this.savedOriginalNbt = tag.getCompound("original_machine_nbt"); + } + } + + @Override + protected void saveCommon(CompoundTag tag) { + super.saveCommon(tag); + if (originalMachineBlock != null) { + tag.putString("original_machine_block", originalMachineBlock.toString()); } + if (savedOriginalNbt != null) { + tag.put("original_machine_nbt", savedOriginalNbt); + } + } + + @Override + protected void applyImplicitComponents(DataComponentInput componentInput) { + super.applyImplicitComponents(componentInput); + CustomData customData = componentInput.get(DataComponents.CUSTOM_DATA); + if (customData != null) { + loadCommon(customData.copyTag()); + } + } + + @Override + protected void collectImplicitComponents(DataComponentMap.Builder builder) { + super.collectImplicitComponents(builder); + CompoundTag tag = new CompoundTag(); + saveCommon(tag); + builder.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); } } diff --git a/src/main/java/com/compactmachinespor/block/FactoryBlock.java b/src/main/java/com/compactmachinespor/block/FactoryBlock.java index d8df72f..b73d441 100644 --- a/src/main/java/com/compactmachinespor/block/FactoryBlock.java +++ b/src/main/java/com/compactmachinespor/block/FactoryBlock.java @@ -1,8 +1,14 @@ package com.compactmachinespor.block; import com.compactmachinespor.Cyumocompactmachinespor; +import com.compactmachinespor.core.Core; import com.mojang.serialization.MapCodec; import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BaseEntityBlock; import net.minecraft.world.level.block.RenderShape; @@ -12,6 +18,7 @@ import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.MapColor; +import net.minecraft.world.phys.BlockHitResult; import org.jetbrains.annotations.Nullable; public class FactoryBlock extends BaseEntityBlock { @@ -37,6 +44,29 @@ protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; } + /** + * 启动棒右键工厂方块 → 还原为原来的紧凑空间机器方块 + */ + @Override + protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hitResult) { + if (!stack.is(Cyumocompactmachinespor.LAUNCHER_STICK)) { + return super.useItemOn(stack, state, level, pos, player, hand, hitResult); + } + if (level.isClientSide) { + return ItemInteractionResult.SUCCESS; + } + if (level.getBlockEntity(pos) instanceof FactoryBlockEntity factoryBe) { + String roomCode = factoryBe.getRoomCode(); + if (roomCode != null && !roomCode.isEmpty() && level instanceof ServerLevel serverLevel) { + Core.revertToBoundMachine(serverLevel, pos, roomCode); + stack.shrink(1); + player.inventoryMenu.broadcastChanges(); + return ItemInteractionResult.SUCCESS; + } + } + return super.useItemOn(stack, state, level, pos, player, hand, hitResult); + } + @Nullable @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { diff --git a/src/main/java/com/compactmachinespor/block/FactoryBlockEntity.java b/src/main/java/com/compactmachinespor/block/FactoryBlockEntity.java index a6ce0cc..f4271fb 100644 --- a/src/main/java/com/compactmachinespor/block/FactoryBlockEntity.java +++ b/src/main/java/com/compactmachinespor/block/FactoryBlockEntity.java @@ -327,6 +327,9 @@ protected void loadCommon(CompoundTag tag) { loadFluidMap(tag, "output_fluids", outputFluids); inputEnergy = loadEnergy(tag, "input_energy"); outputEnergy = loadEnergy(tag, "output_energy"); + if (tag.contains("original_attachments")) { + originalAttachments = tag.getCompound("original_attachments"); + } updateLists(); } @@ -339,6 +342,9 @@ protected void saveCommon(CompoundTag tag) { saveFluidMap(tag, "output_fluids", outputFluids); saveEnergy(tag, "input_energy", inputEnergy); saveEnergy(tag, "output_energy", outputEnergy); + if (originalAttachments != null) { + tag.put("original_attachments", originalAttachments); + } } public IItemHandler getItemHandler() { @@ -560,6 +566,18 @@ public static EnergyStorage loadEnergy(CompoundTag tag, String key) { return null; } + // 原始 machine_color 所在 neoforge:attachments,由 Core.finish() 设置 + private CompoundTag originalAttachments; + + public CompoundTag getOriginalAttachments() { + return originalAttachments; + } + + public void setOriginalAttachments(CompoundTag attachments) { + this.originalAttachments = attachments; + setChanged(); + } + public List> getForShow() { return List.of(inputItemList, inputFluidList, outputItemList, outputFluidList); } diff --git a/src/main/java/com/compactmachinespor/core/Core.java b/src/main/java/com/compactmachinespor/core/Core.java index a8fb90e..22d8cad 100644 --- a/src/main/java/com/compactmachinespor/core/Core.java +++ b/src/main/java/com/compactmachinespor/core/Core.java @@ -1,9 +1,12 @@ package com.compactmachinespor.core; +import com.compactmachinespor.Config; import com.compactmachinespor.Cyumocompactmachinespor; +import com.compactmachinespor.block.EvaluatorBlockEntity; import com.compactmachinespor.block.BaseIOBlock; import com.compactmachinespor.block.BaseIOBlockEntity; import com.compactmachinespor.block.FactoryBlockEntity; +import com.compactmachinespor.core.Machine.InventorySnapshot; import dev.compactmods.machines.api.CompactMachines; import dev.compactmods.machines.api.component.CMDataComponents; import dev.compactmods.machines.api.dimension.CompactDimension; @@ -12,20 +15,32 @@ import dev.compactmods.machines.api.room.spatial.IRoomBoundaries; import dev.compactmods.machines.server.CompactMachinesServer; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; +import net.minecraft.nbt.CompoundTag; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.AABB; +import net.neoforged.neoforge.capabilities.Capabilities; +import net.neoforged.neoforge.energy.IEnergyStorage; +import net.neoforged.neoforge.fluids.capability.IFluidHandler; +import net.neoforged.neoforge.items.IItemHandler; import net.neoforged.neoforge.server.ServerLifecycleHooks; import javax.annotation.Nullable; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -35,6 +50,12 @@ public class Core { + + public enum CreateResult { + SUCCESS, + ABORTED_ROOM_ALREADY_EXISTS, + ABORTED_SUSPICIOUS_BLOCKS + } private static final Map MACHINES = new ConcurrentHashMap<>(); private static final Map ROOM2UUID = new ConcurrentHashMap<>(); @@ -42,15 +63,46 @@ public static Map getMachines() { return MACHINES; } - public static void createMachine(ServerLevel overworldLevel, String roomCode, BlockPos targetPos) { - if (MACHINES.containsKey(roomCode)) return; - MACHINES.put(roomCode, new Machine(getTicks(overworldLevel), roomCode, targetPos)); - ROOM2UUID.put(roomCode, UUID.randomUUID()); + public static CreateResult createMachine(ServerLevel overworldLevel, String roomCode, BlockPos targetPos) { + if (MACHINES.containsKey(roomCode)) return CreateResult.ABORTED_ROOM_ALREADY_EXISTS; + ServerLevel compactWorld = overworldLevel.getServer().getLevel(CompactDimension.LEVEL_KEY); + if (compactWorld == null) return CreateResult.ABORTED_SUSPICIOUS_BLOCKS; + + // 必须先生成 UUID,再 loadRoom(forceRoom 需要 UUID 来强制加载区块) + UUID machineUUID = UUID.randomUUID(); + ROOM2UUID.put(roomCode, machineUUID); loadRoom(compactWorld, roomCode); + + // 第一层:黑名单检测 + if (hasSuspiciousBlocks(compactWorld, roomCode)) { + unLoadRoom(compactWorld, roomCode); + ROOM2UUID.remove(roomCode); + return CreateResult.ABORTED_SUSPICIOUS_BLOCKS; + } + + MACHINES.put(roomCode, new Machine(getTicks(overworldLevel), roomCode, targetPos)); + + // 保存原机器的 neoforge:attachments(含 machine_color)用于工厂还原 + BlockEntity evBe = overworldLevel.getBlockEntity(targetPos); + if (evBe instanceof EvaluatorBlockEntity evaluator) { + CompoundTag savedNbt = evaluator.getSavedOriginalNbt(); + if (savedNbt != null && savedNbt.contains("neoforge:attachments")) { + getMachine(roomCode).originalAttachments = savedNbt.getCompound("neoforge:attachments").copy(); + } + } + + // 第二层:库存基线审计 — 记录 S0 + if (Config.ENABLE_INVENTORY_AUDIT.get()) { + Machine machine = getMachine(roomCode); + machine.inventoryStart = scanInventory(compactWorld, roomCode); + } + scanRoom(compactWorld, roomCode); + return CreateResult.SUCCESS; } + public static Machine getMachine(String roomCode) { return MACHINES.get(roomCode); } @@ -144,6 +196,12 @@ public static void finish(String roomCode, BlockPos overworldPos) { return; } Machine machine = getMachine(roomCode); + + // 库存基线审计 — 记录 S1 并修正产出 + if (Config.ENABLE_INVENTORY_AUDIT.get() && machine != null) { + machine.inventoryEnd = scanInventory(compactWorld, roomCode); + } + Map, Double> inputData = calculate(machine.InputData); if (machine.EnergyData != null) { inputData.put(Holder.direct(null), RateEvaluator.evaluateStableRate(machine.EnergyData.getFirst().data())); @@ -151,6 +209,12 @@ public static void finish(String roomCode, BlockPos overworldPos) { Map, Double> outputData = calculate(machine.OutputData); if (machine.EnergyData != null) outputData.put(Holder.direct(null), RateEvaluator.evaluateStableRate(machine.EnergyData.getLast().data())); + + // 用库存基线审计修正 input/output 数据 + if (Config.ENABLE_INVENTORY_AUDIT.get()) { + auditProduction(machine, inputData, outputData); + } + machine.IOBlocks.forEach( pos -> compactWorld.setBlock( @@ -167,6 +231,10 @@ public static void finish(String roomCode, BlockPos overworldPos) { FactoryBlockEntity be = (FactoryBlockEntity) Objects.requireNonNull(overworld.getBlockEntity(overworldPos)); be.setRoomCode(roomCode); be.initTanks(inputData, outputData); + // 传递原始 machine_color 附件数据到工厂方块 + if (machine.originalAttachments != null) { + be.setOriginalAttachments(machine.originalAttachments); + } MACHINES.remove(roomCode); ROOM2UUID.remove(roomCode); compactWorld.getChunkSource().tick(() -> true, false); @@ -196,4 +264,268 @@ public static ItemStack unpackToItem(String roomCode) { return stack; } -} + // ========== 反作弊:黑名单检测 ========== + + /** + * 扫描房间内是否包含黑名单中的可疑 Mod/方块 + */ + private static boolean hasSuspiciousBlocks(ServerLevel compactWorld, String roomCode) { + var suspiciousMods = Config.SUSPICIOUS_MODS.get(); + var suspiciousBlocks = Config.SUSPICIOUS_BLOCKS.get(); + if (suspiciousMods.isEmpty() && suspiciousBlocks.isEmpty()) return false; + + AABB roomAABB = Objects.requireNonNull(getRoomBoundaries(compactWorld, roomCode)).outerBounds(); + int startX = (int) Math.floor(roomAABB.minX); + int startY = (int) Math.floor(roomAABB.minY); + int startZ = (int) Math.floor(roomAABB.minZ); + int endX = (int) Math.floor(roomAABB.maxX - EPSILON); + int endY = (int) Math.floor(roomAABB.maxY - EPSILON); + int endZ = (int) Math.floor(roomAABB.maxZ - EPSILON); + + for (int x = startX; x <= endX; x++) { + for (int y = startY; y <= endY; y++) { + for (int z = startZ; z <= endZ; z++) { + BlockPos pos = new BlockPos(x, y, z); + BlockState state = compactWorld.getBlockState(pos); + if (state.isAir()) continue; + ResourceLocation id = BuiltInRegistries.BLOCK.getKey(state.getBlock()); + if (suspiciousMods.contains(id.getNamespace())) return true; + if (suspiciousBlocks.contains(id.toString())) return true; + } + } + } + return false; + } + + // ========== 反作弊:库存基线审计 ========== + + /** + * 扫描房间内所有容器的库存,生成快照(S0 或 S1) + * 使用 IdentityHashMap 对 IItemHandler 引用去重(多方块容器) + */ + public static InventorySnapshot scanInventory(ServerLevel compactWorld, String roomCode) { + var roomAABB = Objects.requireNonNull(getRoomBoundaries(compactWorld, roomCode)).outerBounds(); + int startX = (int) Math.floor(roomAABB.minX); + int startY = (int) Math.floor(roomAABB.minY); + int startZ = (int) Math.floor(roomAABB.minZ); + int endX = (int) Math.floor(roomAABB.maxX - EPSILON); + int endY = (int) Math.floor(roomAABB.maxY - EPSILON); + int endZ = (int) Math.floor(roomAABB.maxZ - EPSILON); + + Map, Long> items = new ConcurrentHashMap<>(); + Map, Long> fluids = new ConcurrentHashMap<>(); + long energy = 0; + + // 引用恒等去重:同一个 handler 对象只扫一次 + Set seenItemHandlers = Collections.newSetFromMap(new IdentityHashMap<>()); + Set seenFluidHandlers = Collections.newSetFromMap(new IdentityHashMap<>()); + Set seenEnergyHandlers = Collections.newSetFromMap(new IdentityHashMap<>()); + + // 本模组方块列表,跳過不扫 + var ownBlocks = Set.of( + Cyumocompactmachinespor.INPUT_BLOCK.get(), + Cyumocompactmachinespor.OUTPUT_BLOCK.get(), + Cyumocompactmachinespor.EVALUATOR_BLOCK.get(), + Cyumocompactmachinespor.FACTORY_BLOCK.get() + ); + + for (int x = startX; x <= endX; x++) { + for (int y = startY; y <= endY; y++) { + for (int z = startZ; z <= endZ; z++) { + BlockPos pos = new BlockPos(x, y, z); + BlockState state = compactWorld.getBlockState(pos); + if (state.isAir() || ownBlocks.contains(state.getBlock())) continue; + + // 扫 IItemHandler + for (Direction dir : Direction.values()) { + IItemHandler itemHandler = compactWorld.getCapability(Capabilities.ItemHandler.BLOCK, pos, dir); + if (itemHandler != null && seenItemHandlers.add(itemHandler)) { + for (int slot = 0; slot < itemHandler.getSlots(); slot++) { + var stack = itemHandler.getStackInSlot(slot); + if (!stack.isEmpty()) { + items.merge(stack.getItemHolder(), (long) stack.getCount(), Long::sum); + } + } + break; + } + } + + // 扫 IFluidHandler + for (Direction dir : Direction.values()) { + IFluidHandler fluidHandler = compactWorld.getCapability(Capabilities.FluidHandler.BLOCK, pos, dir); + if (fluidHandler != null && seenFluidHandlers.add(fluidHandler)) { + for (int tank = 0; tank < fluidHandler.getTanks(); tank++) { + var fluidStack = fluidHandler.getFluidInTank(tank); + if (!fluidStack.isEmpty()) { + fluids.merge(fluidStack.getFluidHolder(), (long) fluidStack.getAmount(), Long::sum); + } + } + break; + } + } + + // 扫 IEnergyStorage + for (Direction dir : Direction.values()) { + IEnergyStorage energyStorage = compactWorld.getCapability(Capabilities.EnergyStorage.BLOCK, pos, dir); + if (energyStorage != null && seenEnergyHandlers.add(energyStorage)) { + energy += energyStorage.getEnergyStored(); + break; + } + } + } + } + } + + return new InventorySnapshot(items, fluids, energy); + } + + /** + * 用库存基线审计修正 RateEvaluator 的产出数据 + * realProduction[X] = S1[X] - S0[X] + O[X] - I[X] + */ + private static void auditProduction(Machine machine, Map, Double> inputData, Map, Double> outputData) { + if (machine.inventoryStart == null || machine.inventoryEnd == null) return; + + InventorySnapshot s0 = machine.inventoryStart; + InventorySnapshot s1 = machine.inventoryEnd; + + // 处理物品 + for (var entry : outputData.entrySet()) { + Holder id = entry.getKey(); + if (id.value() == null) continue; // 能量已单独处理 + + // 用 Holder 的 item 注册名作为 key 从 S0/S1 中查找 + long s0Count = s0.items().getOrDefault(id, 0L); + long s1Count = s1.items().getOrDefault(id, 0L); + long totalO = machine.totalOutput.getOrDefault(id, 0L); + long totalI = machine.totalInput.getOrDefault(id, 0L); + + // realProduction = S1 - S0 + O - I + long realTotal = s1Count - s0Count + totalO - totalI; + + if (realTotal <= 0) { + // 该物品没有被真正产出,从 output 移除 + Cyumocompactmachinespor.LOGGER.debug("Audit: {} realProduction={}, removing from output", id, realTotal); + entry.setValue(0.0); + } else { + // 用真实产出比例修正 RateEvaluator 算出的每秒速率 + double recordedTotal = totalO; + if (recordedTotal > 0) { + double ratio = (double) realTotal / recordedTotal; + entry.setValue(entry.getValue() * ratio); + Cyumocompactmachinespor.LOGGER.debug("Audit: {} scaled by {} (real={}, recorded={})", id, ratio, realTotal, recordedTotal); + } + } + } + + // 处理能量 + Holder energyKey = Holder.direct(null); + if (outputData.containsKey(energyKey)) { + long s0Energy = s0.energy(); + long s1Energy = s1.energy(); + long totalOEnergy = machine.totalOutput.getOrDefault(energyKey, 0L); + long totalIEnergy = machine.totalInput.getOrDefault(energyKey, 0L); + long realEnergy = s1Energy - s0Energy + totalOEnergy - totalIEnergy; + + if (realEnergy <= 0) { + outputData.put(energyKey, 0.0); + } else if (totalOEnergy > 0) { + double ratio = (double) realEnergy / totalOEnergy; + outputData.put(energyKey, outputData.get(energyKey) * ratio); + } + } + + // 将消耗物(负 realProduction)归入 inputData + for (var entry : s0.items().entrySet()) { + Holder id = entry.getKey(); + if (id.value() == null) continue; + + long s0Count = s0.items().getOrDefault(id, 0L); + long s1Count = s1.items().getOrDefault(id, 0L); + long totalO = machine.totalOutput.getOrDefault(id, 0L); + long totalI = machine.totalInput.getOrDefault(id, 0L); + long realTotal = s1Count - s0Count + totalO - totalI; + + if (realTotal < 0 && !outputData.containsKey(id)) { + // 该物品被消耗了,且不是产出物 → 归入 inputData + double rate = RateEvaluator.evaluateStableRate( + machine.InputData.getOrDefault(id, new Machine.Data(new int[1])).data() + ); + if (rate <= 0) { + // RateEvaluator 没有记录到消耗速率,用平均速率近似 + long totalTicks = getTicks(Objects.requireNonNull( + ServerLifecycleHooks.getCurrentServer().getLevel(Level.OVERWORLD))); + long elapsed = machine.StartTick.get() >= 0 ? totalTicks - machine.StartTick.get() : 1; + if (elapsed > 0) { + rate = (double) (-realTotal) / elapsed; + } + } + if (rate > 0) { + inputData.put(id, rate); + } + } + } + } + + // ========== 工厂方块 → 还原为紧凑空间机器 ========== + + /** + * 将工厂方块还原为原来的紧缩空间机器方块 + * 由 FactoryBlock.useItemOn(启动棒) 调用 + * 通过 ItemStack.save 创建包含 data components 的 NBT,再用 loadWithComponents 载入 + */ + public static void revertToBoundMachine(ServerLevel level, BlockPos pos, String roomCode) { + // 从当前位置的 FactoryBlockEntity 读取保存的原始 attachments + CompoundTag savedAttachments = null; + net.minecraft.world.level.block.entity.BlockEntity existingBe = level.getBlockEntity(pos); + if (existingBe instanceof FactoryBlockEntity fbe) { + savedAttachments = fbe.getOriginalAttachments(); + } + + Block machineBlock = ((net.minecraft.world.item.BlockItem) BOUND_MACHINE.get()).getBlock(); + if (machineBlock == null) { + Cyumocompactmachinespor.LOGGER.error("Cannot find BoundCompactMachineBlock from BOUND_MACHINE item"); + return; + } + + // 先放置方块,获取默认 BE 的类型 ID + level.removeBlockEntity(pos); + level.setBlockAndUpdate(pos, machineBlock.defaultBlockState()); + + net.minecraft.world.level.block.entity.BlockEntity defaultBe = level.getBlockEntity(pos); + if (defaultBe == null) { + Cyumocompactmachinespor.LOGGER.warn("No BE created at {}", pos); + return; + } + + // 构建正确的 NBT:room_code 在根级,machine_color 在 neoforge:attachments + String beTypeId = net.minecraft.core.registries.BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(defaultBe.getType()).toString(); + CompoundTag beNbt = new CompoundTag(); + beNbt.putString("id", beTypeId); + beNbt.putInt("x", pos.getX()); + beNbt.putInt("y", pos.getY()); + beNbt.putInt("z", pos.getZ()); + beNbt.putString("room_code", roomCode); + + if (savedAttachments != null) { + beNbt.put("neoforge:attachments", savedAttachments.copy()); + } else { + CompoundTag defaultAttachments = new CompoundTag(); + defaultAttachments.putString("compactmachines:machine_color", "#C95B13"); + beNbt.put("neoforge:attachments", defaultAttachments); + } + + net.minecraft.world.level.block.entity.BlockEntity loaded = + net.minecraft.world.level.block.entity.BlockEntity.loadStatic(pos, level.getBlockState(pos), beNbt, level.registryAccess()); + if (loaded != null) { + level.setBlockEntity(loaded); + loaded.setChanged(); + Cyumocompactmachinespor.LOGGER.info("FactoryBlock reverted at {} (room={})", pos, roomCode); + } else { + Cyumocompactmachinespor.LOGGER.warn("loadStatic returned null at {}", pos); + } + + level.sendBlockUpdated(pos, level.getBlockState(pos), level.getBlockState(pos), Block.UPDATE_ALL); + } + +} \ No newline at end of file diff --git a/src/main/java/com/compactmachinespor/core/Machine.java b/src/main/java/com/compactmachinespor/core/Machine.java index 5156d02..8820fec 100644 --- a/src/main/java/com/compactmachinespor/core/Machine.java +++ b/src/main/java/com/compactmachinespor/core/Machine.java @@ -2,12 +2,14 @@ import com.compactmachinespor.Config; import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; import net.minecraft.core.Holder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; public class Machine { @@ -21,6 +23,15 @@ public class Machine { public List EnergyData = null; + // 库存基线审计字段 + public final Map, Long> totalInput = new ConcurrentHashMap<>(); + public final Map, Long> totalOutput = new ConcurrentHashMap<>(); + public InventorySnapshot inventoryStart; + public InventorySnapshot inventoryEnd; + + // 原机器的 neoforge:attachments(含 machine_color),由 createMachine 从 EvaluatorBlockEntity 读取 + public CompoundTag originalAttachments; + public static final int EVALUATE_SECONDS = Config.EVALUATE_SECONDS.get(); public Machine(long startTick, String roomCode, BlockPos targetPos) { @@ -83,6 +94,34 @@ public void addEnergyData(DataSetType type, int data, long currentTick) { } } + /** + * 记录累计 IO 总量(用于库存基线审计) + */ + public void addTotal(DataSetType type, Holder id, long amount) { + Map, Long> target = switch (type) { + case Input -> totalInput; + case Output -> totalOutput; + }; + target.merge(id, amount, Long::sum); + } + + public void addTotalEnergy(DataSetType type, long amount) { + Map, Long> target = switch (type) { + case Input -> totalInput; + case Output -> totalOutput; + }; + target.merge(Holder.direct(null), amount, Long::sum); + } + + /** + * 房间库存快照,由 Core.scanInventory() 生成 + */ + public record InventorySnapshot(Map, Long> items, Map, Long> fluids, long energy) { + public static InventorySnapshot empty() { + return new InventorySnapshot(new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), 0); + } + } + public enum DataSetType { Input, Output } diff --git a/src/main/java/com/compactmachinespor/mixin/BoundCompactMachineBlockMixin.java b/src/main/java/com/compactmachinespor/mixin/BoundCompactMachineBlockMixin.java index 62a34ff..d1b3d92 100644 --- a/src/main/java/com/compactmachinespor/mixin/BoundCompactMachineBlockMixin.java +++ b/src/main/java/com/compactmachinespor/mixin/BoundCompactMachineBlockMixin.java @@ -6,6 +6,9 @@ import dev.compactmods.machines.machine.block.BoundCompactMachineBlock; import dev.compactmods.machines.machine.block.BoundCompactMachineBlockEntity; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.InteractionHand; import net.minecraft.world.ItemInteractionResult; @@ -36,9 +39,16 @@ private void interceptUseItemOn( stack.shrink(1); if (!level.isClientSide && level instanceof ServerLevel serverLevel) { player.inventoryMenu.broadcastChanges(); - String roomCode = ((BoundCompactMachineBlockEntity) Objects.requireNonNull(level.getBlockEntity(pos))).connectedRoom(); + BoundCompactMachineBlockEntity cmBe = (BoundCompactMachineBlockEntity) Objects.requireNonNull(level.getBlockEntity(pos)); + String roomCode = cmBe.connectedRoom(); + ResourceLocation originalBlockId = BuiltInRegistries.BLOCK.getKey(state.getBlock()); + // 保存完整的 BE NBT 用于还原(含 data components) + CompoundTag savedNbt = cmBe.saveWithId(serverLevel.registryAccess()); + Core.replaceBlock(serverLevel, pos, Cyumocompactmachinespor.EVALUATOR_BLOCK); - ((EvaluatorBlockEntity) Objects.requireNonNull(serverLevel.getBlockEntity(pos))).setRoomCode(roomCode); + EvaluatorBlockEntity evBe = (EvaluatorBlockEntity) Objects.requireNonNull(serverLevel.getBlockEntity(pos)); + evBe.setRoomCode(roomCode); + evBe.saveOriginalMachine(originalBlockId, savedNbt); cir.setReturnValue(ItemInteractionResult.SUCCESS); } } diff --git a/src/main/resources/assets/compactmachinespor/lang/en_us.json b/src/main/resources/assets/compactmachinespor/lang/en_us.json index ac6dc12..cecaf67 100644 --- a/src/main/resources/assets/compactmachinespor/lang/en_us.json +++ b/src/main/resources/assets/compactmachinespor/lang/en_us.json @@ -26,5 +26,9 @@ "chat.compactmachinespor.item": "Item", "chat.compactmachinespor.fluid": "Fluid", "chat.compactmachinespor.added": "added", - "chat.compactmachinespor.removed": "removed" + "chat.compactmachinespor.removed": "removed", + "chat.compactmachinespor.suspicious_blocks": "§c[CM] Room contains unscannable storage (AE2/RS/etc). Evaluation aborted.", + "config.compactmachinespor.enable_inventory_audit": "Enable Inventory Audit", + "config.compactmachinespor.suspicious_mods": "Suspicious Mod IDs", + "config.compactmachinespor.suspicious_blocks": "Suspicious Block IDs" } diff --git a/src/main/resources/assets/compactmachinespor/lang/zh_cn.json b/src/main/resources/assets/compactmachinespor/lang/zh_cn.json index dee8ea1..42f621e 100644 --- a/src/main/resources/assets/compactmachinespor/lang/zh_cn.json +++ b/src/main/resources/assets/compactmachinespor/lang/zh_cn.json @@ -26,5 +26,9 @@ "chat.compactmachinespor.item": "物品", "chat.compactmachinespor.fluid": "流体", "chat.compactmachinespor.added": "添加", - "chat.compactmachinespor.removed": "移除" + "chat.compactmachinespor.removed": "移除", + "chat.compactmachinespor.suspicious_blocks": "§c[压缩空间] 房间内包含不可审计的存储模组(AE2/RS等),评估已终止。", + "config.compactmachinespor.enable_inventory_audit": "启用库存审计", + "config.compactmachinespor.suspicious_mods": "可疑模组ID", + "config.compactmachinespor.suspicious_blocks": "可疑方块ID" } From c7602c221c14af423803500e32fc3ef41f9e2c8d Mon Sep 17 00:00:00 2001 From: Yansunsky <1321833118@qq.com> Date: Thu, 11 Jun 2026 03:17:06 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20EnableFactoryR?= =?UTF-8?q?evert=20=E9=85=8D=E7=BD=AE=E5=BC=80=E5=85=B3=EF=BC=88=E9=BB=98?= =?UTF-8?q?=E8=AE=A4false=E7=A6=81=E7=94=A8=E5=90=AF=E5=8A=A8=E6=A3=92?= =?UTF-8?q?=E8=BF=98=E5=8E=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增配置项 EnableFactoryRevert (默认 false,禁止启动棒将工厂还原为空间方块) - 防止物品复制Bug的同时不破坏评估系统的正常工作 - 后续根据快照修复进度重新开启 --- src/main/java/com/compactmachinespor/Config.java | 5 +++++ .../java/com/compactmachinespor/block/FactoryBlock.java | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/main/java/com/compactmachinespor/Config.java b/src/main/java/com/compactmachinespor/Config.java index 92cc746..05d8a59 100644 --- a/src/main/java/com/compactmachinespor/Config.java +++ b/src/main/java/com/compactmachinespor/Config.java @@ -40,5 +40,10 @@ public class Config { .translation("config.compactmachinespor.suspicious_blocks") .defineListAllowEmpty("SuspiciousBlocks", java.util.List.of(), o -> o instanceof String); + public static final ModConfigSpec.BooleanValue ENABLE_FACTORY_REVERT = BUILDER + .comment("Allow launcher stick to revert FactoryBlock back to bound machine. Default false to prevent item duplication.") + .translation("config.compactmachinespor.enable_factory_revert") + .define("EnableFactoryRevert", false); + static final ModConfigSpec SPEC = BUILDER.build(); } diff --git a/src/main/java/com/compactmachinespor/block/FactoryBlock.java b/src/main/java/com/compactmachinespor/block/FactoryBlock.java index b73d441..b51de70 100644 --- a/src/main/java/com/compactmachinespor/block/FactoryBlock.java +++ b/src/main/java/com/compactmachinespor/block/FactoryBlock.java @@ -1,9 +1,12 @@ package com.compactmachinespor.block; +import com.compactmachinespor.Config; import com.compactmachinespor.Cyumocompactmachinespor; import com.compactmachinespor.core.Core; import com.mojang.serialization.MapCodec; +import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.InteractionHand; import net.minecraft.world.ItemInteractionResult; @@ -55,6 +58,12 @@ protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Lev if (level.isClientSide) { return ItemInteractionResult.SUCCESS; } + if (!Config.ENABLE_FACTORY_REVERT.get()) { + player.displayClientMessage( + Component.literal("[CM] 工厂还原功能已被禁用").withStyle(ChatFormatting.GRAY), + true); + return ItemInteractionResult.SUCCESS; + } if (level.getBlockEntity(pos) instanceof FactoryBlockEntity factoryBe) { String roomCode = factoryBe.getRoomCode(); if (roomCode != null && !roomCode.isEmpty() && level instanceof ServerLevel serverLevel) {