diff --git a/docs/compact-host-ui-validation/README.md b/docs/compact-host-ui-validation/README.md new file mode 100644 index 00000000..748bae42 --- /dev/null +++ b/docs/compact-host-ui-validation/README.md @@ -0,0 +1,17 @@ +# ECO 紧凑主机界面实机验证 + +环境:Minecraft 1.21.1、NeoForge 21.1.251、ECO 21.2.9、AE2 19.2.18、LDLib2 2.2.40,包含 AE2 Pattern Disk 等开发环境扩展。 + +实机游戏截图与行为测试通过:C9、F9、L9 普通主机页面正常打开;F9 潜行右键打开通讯接口;原生编码合成样板可从背包拿起、放入概览、取出并放回背包,关闭重开后列表与实际槽位一致。插入后服务端鼠标物品为空、概览显示样板;取出后服务端鼠标持有样板、概览槽位为空。 + +样板验收范围:AE2 原生合成样板,配方为橡木原木 → 4 个橡木木板。截图直接保存自运行中的游戏客户端帧缓冲。 + +| 页面 | 实机截图 | +| --- | --- | +| 紧凑 F9:原主机 UI 与 9 级上下并行核心纹理 | ![紧凑 F9 普通主机页面](compact-f9-host.png) | +| 紧凑 C9:原主机 UI 与 9 级上、中、下硬件纹理 | ![紧凑 C9 普通主机页面](compact-c9-host.png) | +| 紧凑 L9:原存储主机 UI | ![紧凑 L9 普通主机页面](compact-l9-host.png) | +| F9 通讯接口:放入样板,重开后仍可读取 | ![样板插入后](compact-pattern-inserted.png) | +| F9 通讯接口:取出样板并放回背包 | ![样板取出后](compact-pattern-extracted.png) | + +构建与单测通过;新版 ECO 的面板状态回归覆盖 9 级纹理、镜像单元格、动态仪表保留及未成形状态。原声明依赖 ECO 21.2.0 编译、单测与打包通过,新版面板尚不存在时跳过对应测试。 diff --git a/docs/compact-host-ui-validation/compact-c9-host.png b/docs/compact-host-ui-validation/compact-c9-host.png new file mode 100644 index 00000000..54e6eec6 Binary files /dev/null and b/docs/compact-host-ui-validation/compact-c9-host.png differ diff --git a/docs/compact-host-ui-validation/compact-f9-host.png b/docs/compact-host-ui-validation/compact-f9-host.png new file mode 100644 index 00000000..dec24819 Binary files /dev/null and b/docs/compact-host-ui-validation/compact-f9-host.png differ diff --git a/docs/compact-host-ui-validation/compact-l9-host.png b/docs/compact-host-ui-validation/compact-l9-host.png new file mode 100644 index 00000000..dd44e78f Binary files /dev/null and b/docs/compact-host-ui-validation/compact-l9-host.png differ diff --git a/docs/compact-host-ui-validation/compact-pattern-extracted.png b/docs/compact-host-ui-validation/compact-pattern-extracted.png new file mode 100644 index 00000000..5637bc8f Binary files /dev/null and b/docs/compact-host-ui-validation/compact-pattern-extracted.png differ diff --git a/docs/compact-host-ui-validation/compact-pattern-inserted.png b/docs/compact-host-ui-validation/compact-pattern-inserted.png new file mode 100644 index 00000000..f7db231a Binary files /dev/null and b/docs/compact-host-ui-validation/compact-pattern-inserted.png differ diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactC9BlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactC9BlockEntity.java index 9407a635..4c7edaab 100644 --- a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactC9BlockEntity.java +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactC9BlockEntity.java @@ -26,7 +26,6 @@ import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowComputationHostBlockEntity; import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowComputationParallelCoreBlockEntity; import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowThreadingCoreBlockEntity; -import com.sorrowmist.useless.compat.neoecoae.compact.ui.CompactSingleBlockUI; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; @@ -383,11 +382,8 @@ public NEComputationNetworkCluster compactNetworkCluster() { @Override public ModularUI createUI(BlockUIMenuType.BlockUIHolder holder) { ModularUI interfaceUI = compactInterface.createUI(holder); - return interfaceUI == null ? CompactSingleBlockUI.create(holder.player, - net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.c9.title"), - net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.c9.overview"), - net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.c9.hardware")) - : interfaceUI; + // 普通右键沿用 ECO C9 主机面板;只有潜行右键才切到影子通讯接口页面。 + return interfaceUI == null ? super.createUI(holder) : interfaceUI; } @Override diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9BlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9BlockEntity.java index fd56109a..667f6b2b 100644 --- a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9BlockEntity.java +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9BlockEntity.java @@ -36,7 +36,6 @@ import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowFluidInputHatchBlockEntity; import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowFluidOutputHatchBlockEntity; import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowNodeLink; -import com.sorrowmist.useless.compat.neoecoae.compact.ui.CompactSingleBlockUI; import com.sorrowmist.useless.core.component.ExternalInventoryKind; import com.sorrowmist.useless.core.component.ExternalInventoryReference; import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; @@ -756,11 +755,8 @@ public void receivePatternPreview(CompoundTag payload) { @Override public ModularUI createUI(BlockUIMenuType.BlockUIHolder holder) { ModularUI interfaceUI = compactInterface.createUI(holder); - return interfaceUI == null ? CompactSingleBlockUI.create(holder.player, - net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.f9.title"), - net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.f9.overview"), - net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.f9.interaction")) - : interfaceUI; + // 普通右键沿用 ECO F9 主机面板;只有潜行右键才切到影子通讯接口页面。 + return interfaceUI == null ? super.createUI(holder) : interfaceUI; } @Override diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactL9BlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactL9BlockEntity.java index d7fdd036..ec562c2a 100644 --- a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactL9BlockEntity.java +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactL9BlockEntity.java @@ -21,7 +21,6 @@ import com.sorrowmist.useless.compat.neoecoae.compact.calculator.CompactHost; import com.sorrowmist.useless.compat.neoecoae.compact.cluster.CompactStorageCluster; import com.sorrowmist.useless.compat.neoecoae.compact.shadow.CompactInterfaceAttachment; -import com.sorrowmist.useless.compat.neoecoae.compact.ui.CompactSingleBlockUI; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; @@ -333,11 +332,8 @@ private UUID stableDomainId(ServerLevel level) { @Override public ModularUI createUI(BlockUIMenuType.BlockUIHolder holder) { ModularUI interfaceUI = compactInterface.createUI(holder); - return interfaceUI == null ? CompactSingleBlockUI.create(holder.player, - net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.l9.title"), - net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.l9.overview"), - net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.l9.interaction")) - : interfaceUI; + // 普通右键沿用 ECO L9 主机面板;只有潜行右键才切到影子通讯接口页面。 + return interfaceUI == null ? super.createUI(holder) : interfaceUI; } @Override diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/ui/CompactHostPanelState.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/ui/CompactHostPanelState.java new file mode 100644 index 00000000..fd23ee89 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/ui/CompactHostPanelState.java @@ -0,0 +1,61 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.ui; + +import net.minecraft.nbt.CompoundTag; + +import java.lang.reflect.Method; + +/** Supplies the ordinary ECO artwork with the compact host's built-in level-nine hardware. */ +public final class CompactHostPanelState { + private static final int SLOT_COUNT = 11; + // ECO panel tiers encode L4/L6/L9 as 1/2/3. + private static final int LEVEL_NINE = 3; + + private record Codec(Method write, Method read) {} + + // These panel classes do not exist in the oldest supported ECO release. Keep their + // linkage inside the optional mixin, using the panel's own public serialization API. + private static final ClassValue CODECS = new ClassValue<>() { + @Override + protected Codec computeValue(Class type) { + try { + return new Codec(type.getMethod("toTag"), type.getMethod("fromTag", CompoundTag.class)); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unsupported ECO host panel " + type.getName(), e); + } + } + }; + + private CompactHostPanelState() {} + + public static Object levelNine(Object panel, boolean computation) { + if (panel == null) return null; + Codec codec = CODECS.get(panel.getClass()); + try { + CompoundTag tag = (CompoundTag) codec.write().invoke(panel); + if (!tag.getBoolean("formed")) return panel; + fillHardware(tag, computation); + return codec.read().invoke(null, tag); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Cannot populate compact ECO host panel", e); + } + } + + static void fillHardware(CompoundTag tag, boolean computation) { + int cells = tag.getInt("cells"); + int tiers = 0; + for (int cell = 0; cell < SLOT_COUNT; cell++) { + if ((cells & (1 << cell)) != 0) tiers |= LEVEL_NINE << (cell * 2); + } + tag.putInt("host", LEVEL_NINE); + tag.putInt("up", tiers); + tag.putInt("low", tiers); + if (computation) { + tag.putInt("core", tiers); + // Newer ECO versions carry explicit members as well as tier bits. Rebuild them + // through ECO's legacy decoder, which selects its original L9 atlas textures. + tag.remove("upperMembers"); + tag.remove("coreMembers"); + tag.remove("lowerMembers"); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/compact/ComputationHostPanelCompactMixin.java b/src/main/java/com/sorrowmist/useless/mixin/compact/ComputationHostPanelCompactMixin.java new file mode 100644 index 00000000..1b8cafc5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/compact/ComputationHostPanelCompactMixin.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.mixin.compact; + +import com.sorrowmist.useless.compat.neoecoae.compact.entity.CompactC9BlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.ui.CompactHostPanelState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(targets = "cn.dancingsnow.neoecoae.blocks.entity.computation.ECOComputationSystemBlockEntity", remap = false) +public abstract class ComputationHostPanelCompactMixin { + @Inject(method = "getComputationControllerPanelState", at = @At("RETURN"), require = 0, remap = false, + cancellable = true) + private void uselessMod$compactHardware(CallbackInfoReturnable cir) { + if ((Object) this instanceof CompactC9BlockEntity) { + cir.setReturnValue(CompactHostPanelState.levelNine(cir.getReturnValue(), true)); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/compact/CraftingHostPanelCompactMixin.java b/src/main/java/com/sorrowmist/useless/mixin/compact/CraftingHostPanelCompactMixin.java new file mode 100644 index 00000000..8cb73b61 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/compact/CraftingHostPanelCompactMixin.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.mixin.compact; + +import com.sorrowmist.useless.compat.neoecoae.compact.entity.CompactF9BlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.ui.CompactHostPanelState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(targets = "cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingSystemBlockEntity", remap = false) +public abstract class CraftingHostPanelCompactMixin { + @Inject(method = "getCraftingControllerPanelState", at = @At("RETURN"), require = 0, remap = false, + cancellable = true) + private void uselessMod$compactHardware(CallbackInfoReturnable cir) { + if ((Object) this instanceof CompactF9BlockEntity) { + cir.setReturnValue(CompactHostPanelState.levelNine(cir.getReturnValue(), false)); + } + } +} diff --git a/src/main/resources/useless_mod.compact.mixins.json b/src/main/resources/useless_mod.compact.mixins.json index 55085b5b..94a1ba94 100644 --- a/src/main/resources/useless_mod.compact.mixins.json +++ b/src/main/resources/useless_mod.compact.mixins.json @@ -4,7 +4,9 @@ "compatibilityLevel": "JAVA_21", "mixins": [ "GridMachineClassMixin", - "MenuDataCodecsCompactMixin" + "MenuDataCodecsCompactMixin", + "CraftingHostPanelCompactMixin", + "ComputationHostPanelCompactMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/ui/CompactHostPanelStateTest.java b/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/ui/CompactHostPanelStateTest.java new file mode 100644 index 00000000..70c2458c --- /dev/null +++ b/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/ui/CompactHostPanelStateTest.java @@ -0,0 +1,94 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.ui; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class CompactHostPanelStateTest { + @Test + void craftingArtworkShowsL9CoresWithoutLosingRuntimeOverlays() throws ReflectiveOperationException { + Class type = panelType("crafting.CraftingControllerPanelState"); + CompoundTag original = new CompoundTag(); + original.putBoolean("formed", true); + original.putInt("host", 3); + original.putInt("cells", (1 << 11) - 1); + original.putBoolean("coolant", true); + original.putBoolean("switch", true); + original.putInt("queue", 573); + original.putInt("energy", 821); + + Object result = CompactHostPanelState.levelNine(read(type, original), false); + CompoundTag updated = write(result); + + assertEquals(original.getInt("cells"), updated.getInt("cells")); + for (int cell = 0; cell < 11; cell++) { + assertEquals(3, tier(result, "upperTier", cell)); + assertEquals(3, tier(result, "lowerTier", cell)); + } + assertTrue(updated.getBoolean("coolant")); + assertTrue(updated.getBoolean("switch")); + assertEquals(573, updated.getInt("queue")); + assertEquals(821, updated.getInt("energy")); + } + + @Test + void computationArtworkRestoresExplicitMembersAndPreservesMirroredCells() throws ReflectiveOperationException { + Class type = panelType("computation.ComputationControllerPanelState"); + CompoundTag original = new CompoundTag(); + original.putBoolean("formed", true); + original.putInt("cells", (1 << 9) | (1 << 10)); + // The spatial lookup returns absent upper/lower members for co-located compact hardware. + original.put("upperMembers", new ListTag()); + original.put("coreMembers", new ListTag()); + original.put("lowerMembers", new ListTag()); + + Object result = CompactHostPanelState.levelNine(read(type, original), true); + + assertEquals(original.getInt("cells"), write(result).getInt("cells")); + assertEquals(3, write(result).getInt("host")); + for (int cell = 0; cell < 11; cell++) { + boolean present = cell >= 9; + for (String row : new String[]{"upper", "core", "lower"}) { + assertEquals(present ? 3 : 0, tier(result, row + "Tier", cell)); + try { + Object member = type.getMethod(row + "Member", int.class).invoke(result, cell); + assertEquals(present, member.getClass().getMethod("present").invoke(member)); + assertNull(member.getClass().getMethod("texture").invoke(member)); + } catch (NoSuchMethodException legacyPanel) { + // ECO versions before explicit members draw directly from the packed tiers. + } + } + } + } + + @Test + void unformedPanelStaysUnformed() throws ReflectiveOperationException { + Class type = panelType("crafting.CraftingControllerPanelState"); + Object unformed = type.getField("UNFORMED").get(null); + assertSame(unformed, CompactHostPanelState.levelNine(unformed, false)); + } + + private static Class panelType(String suffix) throws ClassNotFoundException { + try { + return Class.forName("cn.dancingsnow.neoecoae.gui." + suffix); + } catch (ClassNotFoundException olderEco) { + assumeTrue(false, "This ECO release predates controller artwork"); + throw olderEco; + } + } + + private static Object read(Class type, CompoundTag tag) throws ReflectiveOperationException { + return type.getMethod("fromTag", CompoundTag.class).invoke(null, tag); + } + + private static CompoundTag write(Object panel) throws ReflectiveOperationException { + return (CompoundTag) panel.getClass().getMethod("toTag").invoke(panel); + } + + private static int tier(Object panel, String getter, int cell) throws ReflectiveOperationException { + return (int) panel.getClass().getMethod(getter, int.class).invoke(panel, cell); + } +}