Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/compact-host-ui-validation/README.md
Original file line number Diff line number Diff line change
@@ -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 编译、单测与打包通过,新版面板尚不存在时跳过对应测试。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Codec> 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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Object> cir) {
if ((Object) this instanceof CompactC9BlockEntity) {
cir.setReturnValue(CompactHostPanelState.levelNine(cir.getReturnValue(), true));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Object> cir) {
if ((Object) this instanceof CompactF9BlockEntity) {
cir.setReturnValue(CompactHostPanelState.levelNine(cir.getReturnValue(), false));
}
}
}
4 changes: 3 additions & 1 deletion src/main/resources/useless_mod.compact.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"compatibilityLevel": "JAVA_21",
"mixins": [
"GridMachineClassMixin",
"MenuDataCodecsCompactMixin"
"MenuDataCodecsCompactMixin",
"CraftingHostPanelCompactMixin",
"ComputationHostPanelCompactMixin"
],
"injectors": {
"defaultRequire": 1
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading