用法(完整示例见 {@code wiki/ALLOY_FURNACE_BIGINT_API_ZH_CN.md}):
+ * + *{@code
+ * for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) {
+ * AlloyFurnaceBigIntegerCapacity capacity =
+ * target.capacity(pattern, unitPrototype, requestedCount);
+ * if (!capacity.isAvailable()) {
+ * continue;
+ * }
+ * AlloyFurnaceBigIntegerBatch batch =
+ * target.admit(pattern, unitPrototype, capacity.accepted(), binding);
+ * if (batch != null && batch.commit(unitPrototype)) {
+ * // 材料所有权已转移给机器;产物会切段写回 ME 网络,并通过 binding 回调给你
+ * }
+ * }
+ * }
+ *
+ * 为什么用网格节点遍历而不是 {@code ICraftingService}:AE2 19.2.17 的 + * {@code ICraftingService} 只公开了 {@code getCraftingFor(AEKey)} 与 {@code getCpus()}, + * 没有「枚举全部供应器」的公开接口。遍历 {@code IGrid#getNodes()} 并用 + * {@link IGridNode#getOwner()} 判 {@link AlloyFurnaceBigIntegerProvider} 是唯一稳定的公开路径。
+ * + *仅服务器线程。
+ */ +public final class AlloyFurnaceBigIntegerApi { + private AlloyFurnaceBigIntegerApi() { + } + + /** + * 找出网格里当前可用的全部大数目标,按 {@link AlloyFurnaceBigIntegerTarget#machineIdentity()} 去重。 + * + * @param grid 目标网格;{@code null} 时返回空列表 + * @return 不可变的候选列表;同一台物理机器只会出现一次 + */ + public static @NotNull List供调用方在昂贵的解码之前先做一次廉价过滤;真正的判定仍以 + * {@link AlloyFurnaceBigIntegerTarget#capacity} 的返回值为准。
+ */ + public static @NotNull Set份数用 {@link BigInteger} 表达,可以远超 {@code long} —— 这是本 API 与 AE2 原生 + * {@code ICraftingProvider#pushPattern} 最本质的区别。
+ * + * @param accepted 可接受份数;{@link BigInteger#ZERO} 表示此刻一份都收不了 + * @param statusKey 为 0 时给出的原因(一个语言键,可直接显示给玩家);非 0 时为空串 + */ +public record AlloyFurnaceBigIntegerCapacity(BigInteger accepted, String statusKey) { + + /** 校验并冻结一次容量结果。 */ + public AlloyFurnaceBigIntegerCapacity { + Objects.requireNonNull(accepted, "BigInteger crafting capacity must not be null"); + if (accepted.signum() < 0) { + throw new IllegalArgumentException("BigInteger crafting capacity must not be negative"); + } + statusKey = statusKey == null ? "" : statusKey; + } + + /** @return 本机此刻一份都收不了,并附带原因 */ + public static @NotNull AlloyFurnaceBigIntegerCapacity none(@NotNull String statusKey) { + return new AlloyFurnaceBigIntegerCapacity(BigInteger.ZERO, statusKey); + } + + /** @return 可接受 {@code accepted} 份(必须为正) */ + public static @NotNull AlloyFurnaceBigIntegerCapacity of(@NotNull BigInteger accepted) { + return new AlloyFurnaceBigIntegerCapacity(accepted, ""); + } + + /** @return 此刻是否至少能收一份 */ + public boolean isAvailable() { + return accepted.signum() > 0; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerOutput.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerOutput.java new file mode 100644 index 00000000..29ef97fe --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerOutput.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.api.crafting.bigint; + +import appeng.api.stacks.AEKey; + +import java.math.BigInteger; +import java.util.Objects; + +/** + * 一条 BigInteger 规模的 AE 产物。 + * + *为什么需要它:AE2 自己的存储接口单次最多接受 {@code long} + * ({@code IMEInventory#insert(AEKey, long, Actionable)}),而大数批次的产物总量可以超过 + * {@code long}。本模组内部用 BigInteger 记账、再按 {@code Long.MAX_VALUE} 切段写回网络, + * 对外则用这个不可变记录暴露完整数量,让第三方 CPU 能自己核账,而不必去猜分段。
+ * + *切段粒度为 {@code Long.MAX_VALUE} —— 这是 AE2 存储接口单次能接受的量。
+ * + * @param what 资源键(物品 / 流体 / 其它 AE 键) + * @param amount 精确数量,必须为正 + */ +public record AlloyFurnaceBigIntegerOutput(AEKey what, BigInteger amount) { + + /** 校验并冻结一条大数产物。 */ + public AlloyFurnaceBigIntegerOutput { + Objects.requireNonNull(what, "BigInteger crafting output key must not be null"); + Objects.requireNonNull(amount, "BigInteger crafting output amount must not be null"); + if (amount.signum() <= 0) { + throw new IllegalArgumentException("BigInteger crafting output amount must be positive"); + } + } + + /** @return 该数量是否已超出单个 {@code long} 能表达的范围 */ + public boolean exceedsLong() { + return amount.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerProvider.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerProvider.java new file mode 100644 index 00000000..3bd7511b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerProvider.java @@ -0,0 +1,21 @@ +package com.sorrowmist.useless.api.crafting.bigint; + +import org.jetbrains.annotations.Nullable; + +/** + * 由「支持 BigInteger 批次」的样板供应器方块实体实现的标记接口。 + * + *{@link AlloyFurnaceBigIntegerApi#findTargets} 就是遍历 AE 网格节点、 + * 用 {@code IGridNode#getOwner() instanceof AlloyFurnaceBigIntegerProvider} 找出候选, + * 再向它索取 {@link #bigIntegerTarget()}。这样调用方不需要认识本模组的具体方块实体类, + * 也不需要做反射或 {@code Class.forName}。
+ * + *实现方返回的实例必须仅服务器线程可用。
+ */ +public interface AlloyFurnaceBigIntegerProvider { + + /** + * @return 本机当前可用的 bigint 目标;机器未成形、未联网或链接尚未解析时为 {@code null} + */ + @Nullable AlloyFurnaceBigIntegerTarget bigIntegerTarget(); +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerTarget.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerTarget.java new file mode 100644 index 00000000..7ec40be8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerTarget.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.api.crafting.bigint; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IGrid; +import appeng.api.stacks.KeyCounter; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuBinding; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.Set; + +/** + * 一台支持 BigInteger 批次的多方块万象合金炉,站在「样板供应器」一侧的对外视图。 + * + *拿到实例的唯一正规途径是 {@link AlloyFurnaceBigIntegerApi#findTargets(IGrid)} / + * {@link AlloyFurnaceBigIntegerApi#findTarget(IGrid, String)}。实例由机器的方块实体持有, + * 不要缓存到跨 tick 的字段里,每 tick 重新查一次即可(查询本身是网格节点遍历,很轻)。
+ * + *仅服务器线程:本接口的所有方法都必须在服务器线程调用。
+ */ +public interface AlloyFurnaceBigIntegerTarget { + + /** @return provider 局部稳定的路由身份;同一台机器的生命周期内不变 */ + @NotNull String routeIdentity(); + + /** + * @return provider 无关的物理机器身份,格式 {@code 维度@x,y,z}。 + * + *同一台物理机器无论从哪个 provider 暴露,都必须给出同一个字符串,调用方据此避免超卖 + * 同一台机器。本模组的所有入口都用同一个公式,所以它们认的是同一台机器。
+ */ + @NotNull String machineIdentity(); + + /** @return 本机所在的 AE 网格;机器离线时为 {@code null} */ + @Nullable IGrid grid(); + + /** @return 本机接受的样板种类(AE2 样板物品 id),用于调用方预先过滤 */ + @NotNull Set本方法不产生任何副作用,可以随时调用(例如每次 tick 做一次规划)。
+ * + * @param pattern 已解开的样板(不要把倍率包装类传进来;本 API 只认原始样板) + * @param prototype 单次推送的原型材料,不是整批的 count 倍 + * @param requested 期望的份数,必须为正 + * @return 容量结果;为 0 时 {@link AlloyFurnaceBigIntegerCapacity#statusKey()} 给出原因 + */ + @NotNull AlloyFurnaceBigIntegerCapacity capacity(IPatternDetails pattern, + KeyCounter @NotNull [] prototype, + @NotNull BigInteger requested); + + /** + * 本机是否正在动态降频。 + * + *本模组对每条 bigint 路径都做了「每 tick 时间预算」的动态降频: + * 实测耗时超预算时,后续批次的容量会被按比例收窄。降频状态下 {@link #capacity} 返回的数字 + * 会明显小于机器的真实能力。
+ * + *这个方法的用途就是让调用方区分这两种情况:
+ *注意降频不会将容量降至 0:完全上报「无容量」会引发调度侧「无容量 → 重新提交」的 + * 空转,因此降频只收窄批次规模。是否停止提交由调用方决定。
+ * + * @return 是否正在降频 + */ + boolean isThrottled(); + + /** + * 申请一批大数合成,成功时返回一次性凭据(见 {@link AlloyFurnaceBigIntegerBatch} 的调用契约)。 + * + *本方法不消费材料、不扣能量,只是做一次快照式检查;真正的检查会在 + * {@code commit} 时重新执行(期间机器状态可能变化,最终以 commit 的返回值为准)。
+ * + * @param pattern 已解开的样板 + * @param prototype 单次推送的原型(提交时必须原样传回同一个数组对象) + * @param requested 期望的份数,必须为正 + * @param cpu CPU 侧绑定,用于接收产物回执;{@code null} 表示不需要回调 + * (产物仍会按段写回 ME 网络) + * @return 准入凭据;{@code null} 表示此刻不可接受 + */ + @Nullable AlloyFurnaceBigIntegerBatch admit(IPatternDetails pattern, + KeyCounter @NotNull [] prototype, + @NotNull BigInteger requested, + @Nullable AlloyFurnaceBigIntegerCpuBinding cpu); +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerBatchContext.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerBatchContext.java new file mode 100644 index 00000000..f573b5e8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerBatchContext.java @@ -0,0 +1,50 @@ +package com.sorrowmist.useless.api.crafting.bigint.cpu; + +import appeng.api.crafting.IPatternDetails; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerOutput; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * 一个大数批次的不可变上下文,随每个 CPU 回调一起传入。 + * + *回调可能发生在提交之后的任意 tick(产物要逐段写回网络),所以请用 + * {@link #batchId()} 而不是「当前正在提交的那个批次」来定位状态。
+ * + * @param batchId 批次唯一标识。存档重载后 {@code cpuToken} 会丢失,请用它重新绑定 + * @param cpuToken 调用方在 {@link AlloyFurnaceBigIntegerCpuBinding#cpuToken()} 里给的上下文, + * 原样回传;存档重载后为 {@code null} + * @param machineIdentity 实际执行本批的机器身份({@code 维度@x,y,z}) + * @param pattern 本批使用的样板(原始样板,不是倍率包装类) + * @param acceptedCount 已获准的份数 + * @param plannedOutputs 样板声明的产物(模板键)。动态产物(id-only 产物槽)的实际键 + * 可能与之不同,实际值见 + * {@link AlloyFurnaceBigIntegerCpuAdapter#onBatchOutputs} + * @param energyCharged 提交时实际扣掉的能量 + */ +public record AlloyFurnaceBigIntegerBatchContext(@NotNull UUID batchId, + @Nullable Object cpuToken, + @NotNull String machineIdentity, + @NotNull IPatternDetails pattern, + @NotNull BigInteger acceptedCount, + @NotNull List产物不会丢失 —— 机器在取消时会把队列里剩余的产物强制写回 ME 网络, + * 只是不再走正常的逐 tick 节奏。收到该状态时请按「这批已由机器交付,但我没拿到逐条回执」 + * 处理,不要重复补记产物。
+ */ + CANCELLED, + + /** 批次在提交阶段就被拒绝,材料未被消费。通常不需要处理(因为 {@code commit} 已经返回了 false)。 */ + REJECTED +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuAdapter.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuAdapter.java new file mode 100644 index 00000000..06e61944 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuAdapter.java @@ -0,0 +1,104 @@ +package com.sorrowmist.useless.api.crafting.bigint.cpu; + +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerOutput; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * CPU 侧的产物回执通道:实现它并在 mod 初始化阶段注册,即可收到大数批次的产物与终局通知。 + * + *机器产出的总量可以超过 {@code long},而 AE2 的存储接口单次最多接受 {@code long}, + * 因此产物按 {@code Long.MAX_VALUE} 切段、逐 tick 写回 ME 网络。若 CPU 自行维护 + * BigInteger 账本(例如为「超长合成任务」记账),仅凭网络中的分段无法还原精确总数 —— + * 本通道直接提供完整的 BigInteger 产物。
+ * + *完全没有降级风险:不注册适配器(或 {@code admit} 时传 {@code cpu = null})时, + * 产物仍按段写回 ME 网络,仅不发送这些回调。
+ */ +public interface AlloyFurnaceBigIntegerCpuAdapter { + + /** @return 全局唯一的适配器 id,用于注册去重与持久化 */ + @NotNull ResourceLocation id(); + + /** + * 批次已被机器接受(材料所有权已转移)。 + * + *适合在这里把「本批要产出什么」记进自己的账本,或标记任务进入等待状态。
+ */ + void onBatchAdmitted(@NotNull AlloyFurnaceBigIntegerBatchContext context); + + /** + * 产物已全部写回 ME 网络,下面是本批的实际产出(BigInteger 精确值)。 + * + *对动态产物(id-only 产物槽)而言,这里的键可能与 + * {@link AlloyFurnaceBigIntegerBatchContext#plannedOutputs()} 里的模板键不同 —— + * 请以这里为准。
+ */ + void onBatchOutputs(@NotNull AlloyFurnaceBigIntegerBatchContext context, + @NotNull ListAE2 的存储接口单次只收 {@code long}({@code IMEInventory#insert(AEKey, long, Actionable)}), + * 所以本机默认要把产物按 {@code Long.MAX_VALUE} 切段、逐段写回网络 + * (实测约 2.6µs/段,一 tick 两万多段就是几十毫秒)。若你愿意直接收下整批, + * 这段插入就完全不需要发生。
+ * + *收下的物品不会进入 ME 网络,只存在于你自己的账本里。因此:
+ *出于安全考虑,本机只会在「整趟刷写绑定的都是同一个适配器」时调用本方法; + * 多 CPU 混绑同一台机器时不会调用(退回逐段写回)。
+ * + * @param context 本批上下文(与其它回调同一个) + * @param outputs 本次准备写回网络的产物;可只收其中一部分(返回量 ≤ 这里的量) + * @return 你实际收下的量,按 key 计;未列出的 key 视为不收 + */ + default @NotNull java.util.Map在你自己的 mod 初始化阶段注册一次即可,推荐放在 + * {@code FMLCommonSetupEvent#enqueueWork}(或等价的公共初始化钩子)里:
+ * + *{@code
+ * @SubscribeEvent
+ * public static void onCommonSetup(FMLCommonSetupEvent event) {
+ * event.enqueueWork(() ->
+ * AlloyFurnaceBigIntegerCpuAdapters.register(new MyCpuAdapter()));
+ * }
+ * }
+ *
+ * 注册表是全局的、进程级的,不随存档或世界重载清空。若你的 mod 支持运行期禁用, + * 用 {@link #unregister} 摘掉即可。
+ * + *注册表本身是线程安全的({@code ConcurrentHashMap}),但适配器回调只会在服务器线程发生。 + * 请不要在注册/注销的同时假设回调已停止 —— 注销只影响之后的查找。
+ */ +public final class AlloyFurnaceBigIntegerCpuAdapters { + private static final Map探测该项的原因:本机的「产物交付能力」闸({@code maximumSegmentedCount})假定 + * 产物由本机逐段写回网络。若已有 CPU 适配器整批接走产物,该闸即失去意义 —— + * 交付由对方负责,再按其限制单批规模只会无谓地压小批次。
+ * + *以「声明类是否仍为接口本身」判断 default 方法是否被覆写,是标准做法, + * 调用方因此无需额外实现声明方法。
+ */ + public static boolean hasBulkOutputAdapter() { + for (AlloyFurnaceBigIntegerCpuAdapter adapter : ADAPTERS.values()) { + try { + if (adapter.getClass() + .getMethod("claimOutputs", AlloyFurnaceBigIntegerBatchContext.class, List.class) + .getDeclaringClass() != AlloyFurnaceBigIntegerCpuAdapter.class) { + return true; + } + } catch (NoSuchMethodException ignored) { + // 老版本接口没有这个方法:视为不支持整批接收。 + } + } + return false; + } + + public static @NotNull Map由调用方在 {@code admit(...)} 时构造并传入;机器只把它当不透明的标签, + * 用来在产物交付时找到该回调谁。
+ * + * @param adapterId 已通过 {@link AlloyFurnaceBigIntegerCpuAdapters#register} 注册的适配器 id + * @param cpuToken 调用方自己的上下文(合成任务句柄、CPU 引用等),对机器完全不透明, + * 机器只会原样回传,不做任何检查、比较或序列化。 + * 可以传 {@code null}(例如只有全局回调、不需要区分任务时) + */ +public record AlloyFurnaceBigIntegerCpuBinding(@NotNull ResourceLocation adapterId, + @Nullable Object cpuToken) { + + /** 校验并冻结一次 CPU 绑定。 */ + public AlloyFurnaceBigIntegerCpuBinding { + Objects.requireNonNull(adapterId, "BigInteger crafting CPU adapter id must not be null"); + } + + /** @return 只有适配器 id、没有上下文的绑定 */ + public static @NotNull AlloyFurnaceBigIntegerCpuBinding of(@NotNull ResourceLocation adapterId) { + return new AlloyFurnaceBigIntegerCpuBinding(adapterId, null); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/package-info.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/package-info.java new file mode 100644 index 00000000..1465ff33 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/package-info.java @@ -0,0 +1,67 @@ +/** + * 多方块万象合金炉的 BigInteger 批次 对外公开 API(CPU 侧 SPI)。 + * + *合成 CPU 单次发配的份数可以超过 {@code long},机器也按该数量产出,但 AE2 的 + * 存储接口单次最多接受 {@code long},因此产物按 {@code Long.MAX_VALUE} 切段、逐 tick 写回网络。 + * 若 CPU 自行维护 BigInteger 账本,仅凭网络中的分段无法还原精确总数。
+ * + *实现 {@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapter} + * 并注册,即可收到完整的 BigInteger 产物回执与批次终局通知。
+ * + *| 类型 | 用途 |
|---|---|
| {@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapter} | + *你实现的回调接口 |
| {@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapters} | + *显式注册表(在 mod init 阶段调用一次) |
| {@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuBinding} | + *提交批次时把你的适配器 id 与自己的上下文绑上去 |
| {@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerBatchContext} | + *每次回调都会带上的批次上下文 |
| {@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerBatchResult} | + *批次终局状态 |
{@code
+ * // 1) mod 初始化阶段注册一次
+ * AlloyFurnaceBigIntegerCpuAdapters.register(new MyCpuAdapter());
+ *
+ * // 2) 发配时(服务器线程)
+ * AlloyFurnaceBigIntegerCpuBinding binding =
+ * new AlloyFurnaceBigIntegerCpuBinding(MyCpuAdapter.ID, myCraftingJobHandle);
+ * for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) {
+ * AlloyFurnaceBigIntegerCapacity capacity = target.capacity(pattern, unitPrototype, requested);
+ * if (!capacity.isAvailable()) {
+ * continue;
+ * }
+ * AlloyFurnaceBigIntegerBatch batch = target.admit(pattern, unitPrototype, capacity.accepted(), binding);
+ * if (batch == null) {
+ * continue;
+ * }
+ * if (batch.commit(unitPrototype)) {
+ * break; // 材料所有权已转移;产物会通过 binding 回调给你
+ * }
+ * // commit 失败:回滚你自己扣掉的那 count-1 份,然后试下一台
+ * }
+ * }
+ *
+ * 这个类不参与运行,只是给第三方 mod 作者照抄的样板。真正接入时请替换掉:
+ *请在服务器线程、你自己的 CPU tick 里调用。
+ * + * @param grid 你的 CPU 所在网格 + * @param pattern 已解开的样板(万象样板或 AE2 合成样板) + * @param unitPrototype 单次推送的原型材料,不是整批的 count 倍 + * @param requested 你期望发配的份数,可以超过 {@code long} + * @param jobHandle 你自己的任务句柄,会原样回传给你 + * @param urgent {@code true} 表示紧急批次,即使机器正在降频也照发 + * @return 实际被接受的份数;{@code null} 表示没有任何机器接单 + */ + public static @Nullable BigInteger dispatch(@Nullable IGrid grid, + @NotNull IPatternDetails pattern, + @NotNull KeyCounter @NotNull [] unitPrototype, + @NotNull BigInteger requested, + @Nullable Object jobHandle, + boolean urgent) { + if (grid == null || requested.signum() <= 0) { + return null; + } + AlloyFurnaceBigIntegerCpuBinding binding = new AlloyFurnaceBigIntegerCpuBinding(ID, jobHandle); + for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) { + // 机器正忙(全局每 tick 时间预算已耗尽)时容量收窄,此处按需延后非紧急批次。 + // 降频不会将容量降至 0,是否停止提交由调用方决定。 + if (target.isThrottled() && !urgent) { + continue; + } + AlloyFurnaceBigIntegerCapacity capacity = target.capacity(pattern, unitPrototype, requested); + if (!capacity.isAvailable()) { + // capacity.statusKey() 是可显示给玩家的原因(档次不够 / 缺模具 / 没能量)。 + continue; + } + AlloyFurnaceBigIntegerBatch batch = + target.admit(pattern, unitPrototype, capacity.accepted(), binding); + if (batch == null) { + continue; + } + if (batch.commit(unitPrototype)) { + // 成功:材料所有权已转移,产物会经回调交回来。 + return batch.count(); + } + // 失败:机器没消费任何材料。请回滚你自己扣掉的那 (count - 1) 份,再试下一台。 + // 注意:只有你自己知道该怎么回滚 —— 机器从不碰你那部分账本。 + } + return null; + } + + // ==================== 示例内部实现 ==================== + + private static String jobKey(AlloyFurnaceBigIntegerBatchContext context) { + Object token = context.cpuToken(); + return token == null ? context.batchId().toString() : token.toString(); + } + + /** @return 示例用:某任务累计收到的产出总量 */ + public @Nullable BigInteger deliveredFor(String jobKey) { + return this.deliveredPerJob.get(jobKey); + } + + /** @return 示例用:单条产出里出现过的最大数量 */ + public @NotNull BigInteger largestSingleOutput() { + return this.largestSingleOutput; + } + + /** @return 示例用:当前仍在等待机器交付的批次数 */ + public int pendingBatchCount() { + return this.pendingBatches.size(); + } + + /** + * 注册示例适配器(仅供演示;真实 mod 请在自己初始化阶段调用)。 + */ + @ApiStatus.Experimental + public static void registerSelf() { + AlloyFurnaceBigIntegerCpuAdapters.register(new ExampleBigIntegerCpuAdapter()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/package-info.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/package-info.java new file mode 100644 index 00000000..890a5078 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/package-info.java @@ -0,0 +1,81 @@ +/** + * 多方块万象合金炉的 BigInteger 批次 对外公开 API(供应器侧契约)。 + * + *AE2 原生的 {@code ICraftingProvider#pushPattern} 只能用 {@code long} 表达数量,一次物理提交 + * 最多 {@code Long.MAX / 每次消耗量} 份。想发配真正的大数(例如 {@code 1e22} 个物品)只有两条路:
+ * + *本 API 把第 2 条做成本模组自己的公开契约:任意第三方合成 CPU 只要实现一个适配器接口, + * 即可实现 bigint 发配,不需要依赖任何第三方调度框架,也不需要往 AE2 的 CPU 逻辑里写 mixin。
+ * + *| 类型 | 用途 |
|---|---|
| {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerApi} | + *发现网格里的可用目标 |
| {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget} | + *一台机器:查容量 / 申请批次 |
| {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerCapacity} | + *容量查询结果 |
| {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerBatch} | + *一次性准入凭据,{@code commit} 后所有权转移 |
| {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerOutput} | + *BigInteger 规模的产物(可以超过 long) |
| {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerProvider} | + *由供应器方块实体实现,供发现流程识别(调用方一般不需要碰) |
CPU 侧的产物回执通道见 {@link com.sorrowmist.useless.api.crafting.bigint.cpu}。
+ * + *大数批次的能量计费方式取决于线圈档次,这直接影响容量能有多大:
+ * + *| 线圈 | 计费方式 | 是否限制份数 |
|---|---|---|
| 有用线圈(tier 10) | + *整批一次固定能耗 {@code ceil(配方能耗 / 1024)},与份数无关 | + *不限制(没有能量闸) |
| 其它档次 | + *{@code count × 单份能耗} | + *限制:{@code count ≤ 可用能量 / 单份能耗} |
所以有用线圈上容量只受材料窗口与产物分段预算限制,能吃到完整 bigint 规模; + * 低档次线圈会被内部能量缓冲(容量随档次增长,只有满级才是 {@code Long.MAX_VALUE})压小。 + * {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget#capacity} + * 会如实反映这一点。
+ */ +package com.sorrowmist.useless.api.crafting.bigint; diff --git a/src/main/java/com/sorrowmist/useless/api/entity/BeeTimerAccess.java b/src/main/java/com/sorrowmist/useless/api/entity/BeeTimerAccess.java new file mode 100644 index 00000000..98858fe3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/entity/BeeTimerAccess.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.api.entity; + +/** + * 蜜蜂「是否携带花粉」的写入访问接口。 + * + *{@code Bee.setHasNectar(boolean)} 是包级私有的,只有公开的 {@code hasNectar()} 可读。 + * 造化杖的「生物加速」需要写入它来加速采蜜归巢,由 mixin 侧的 {@code BeeAccessor} 生成实现。
+ * + *为什么接口定义在主代码而不是 mixin 包:本项目的主代码不能直接引用 + * {@code com.sorrowmist.useless.mixin.*} 下的类型(编译期解析不到), + * 既有做法是「主代码定义普通接口 → mixin 实现它」。
+ */ +public interface BeeTimerAccess { + + void uselessMod$setBeeHasNectar(boolean value); +} diff --git a/src/main/java/com/sorrowmist/useless/api/entity/TurtleTimerAccess.java b/src/main/java/com/sorrowmist/useless/api/entity/TurtleTimerAccess.java new file mode 100644 index 00000000..7590dab6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/entity/TurtleTimerAccess.java @@ -0,0 +1,20 @@ +package com.sorrowmist.useless.api.entity; + +/** + * 海龟下蛋倒计时的访问接口。 + * + *{@code Turtle.layEggCounter} 是包级私有字段,原版也没有公开的 setter + * ({@code setHasEgg} / {@code setLayingEgg} 同样包级私有)。造化杖的「生物加速」 + * 需要直接推进它,因此由 mixin 侧的 {@code TurtleAccessor} 生成访问器实现。
+ * + *为什么接口定义在主代码而不是 mixin 包:本项目的主代码不能直接引用 + * {@code com.sorrowmist.useless.mixin.*} 下的类型(编译期解析不到), + * 既有做法是「主代码定义普通接口 → mixin 实现它」,见 + * {@code PendingOmniversalPatternHolder} / {@code PatternEncodingLogicMixin}。
+ */ +public interface TurtleTimerAccess { + + int uselessMod$getLayEggCounter(); + + void uselessMod$setLayEggCounter(int value); +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/AlloyFurnaceMode.java b/src/main/java/com/sorrowmist/useless/api/enums/AlloyFurnaceMode.java new file mode 100644 index 00000000..4f51df28 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/AlloyFurnaceMode.java @@ -0,0 +1,34 @@ +package com.sorrowmist.useless.api.enums; + +import com.mojang.serialization.Codec; + +public enum AlloyFurnaceMode { + NORMAL("normal"), + INSOLATOR("insolator"), + PRESS("press"); + + // 提供给 Codec 的便捷方法 + public static final Codec+ * 用于将GUI中的面配置映射到世界中的实际方向,映射结果随方块水平朝向变化。 + */ +public enum FurnaceFace { + TOP, + LEFT, + FRONT, + RIGHT, + BOTTOM, + BACK; + + public static final int COUNT = values().length; + + /** + * 根据方块的水平朝向,将逻辑面转换为世界方向。 + * + * @param facing 方块的水平朝向(FRONT所指方向) + * @return 世界方向 + */ + public Direction toDirection(Direction facing) { + return switch (this) { + case TOP -> Direction.UP; + case BOTTOM -> Direction.DOWN; + case FRONT -> facing; + case BACK -> facing.getOpposite(); + case LEFT -> facing.getClockWise(); + case RIGHT -> facing.getCounterClockWise(); + }; + } + + /** + * 根据方块的水平朝向,将世界方向转换为逻辑面。 + * + * @param side 世界方向 + * @param facing 方块的水平朝向(FRONT所指方向) + * @return 对应的逻辑面,无法匹配时返回null + */ + @Nullable + public static FurnaceFace fromDirection(Direction side, Direction facing) { + if (side == Direction.UP) return TOP; + if (side == Direction.DOWN) return BOTTOM; + if (side == facing) return FRONT; + if (side == facing.getOpposite()) return BACK; + if (side == facing.getClockWise()) return LEFT; + if (side == facing.getCounterClockWise()) return RIGHT; + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/FurnaceFaceMode.java b/src/main/java/com/sorrowmist/useless/api/enums/FurnaceFaceMode.java new file mode 100644 index 00000000..1fc81bac --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/FurnaceFaceMode.java @@ -0,0 +1,86 @@ +package com.sorrowmist.useless.api.enums; + +/** + * 高级合金炉每个面的输入输出模式。 + *
+ * 每次点击GUI对应区域时在这些状态间循环,用于控制外部物流手段 + * (漏斗、管道等)是否允许从该面输入/输出物品和流体。 + */ +public enum FurnaceFaceMode { + /** 禁止输入输出,UI无变动 */ + DISABLED(-1), + /** 原材料输入:允许外部向输入槽/输入流体槽输入 */ + MATERIAL_INPUT(0), + /** 原材料输出:允许外部从输出槽/输出流体槽抽取 */ + MATERIAL_OUTPUT(12), + /** 原材料输入和输出 */ + MATERIAL_INPUT_OUTPUT(24), + /** 催化剂输入:允许外部向催化剂槽输入 */ + CATALYST_INPUT(36), + /** 模具输入:允许外部向模具槽输入 */ + MOLD_INPUT(48); + + // UI覆盖图在纹理中的U坐标(V固定为265,尺寸11*12),-1表示无覆盖 + private final int overlayU; + + FurnaceFaceMode(int overlayU) { + this.overlayU = overlayU; + } + + /** + * 获取该模式在纹理中的覆盖图U坐标(V=265)。 + * + * @return U坐标,-1表示无覆盖(DISABLED) + */ + public int getOverlayU() { + return this.overlayU; + } + + public boolean hasOverlay() { + return this.overlayU >= 0; + } + + /** 是否允许原材料输入(物品输入槽/输入流体槽) */ + public boolean allowsMaterialInput() { + return this == MATERIAL_INPUT || this == MATERIAL_INPUT_OUTPUT; + } + + /** 是否允许原材料输出(物品输出槽/输出流体槽) */ + public boolean allowsMaterialOutput() { + return this == MATERIAL_OUTPUT || this == MATERIAL_INPUT_OUTPUT; + } + + /** 是否允许催化剂输入 */ + public boolean allowsCatalystInput() { + return this == CATALYST_INPUT; + } + + /** 是否允许模具输入 */ + public boolean allowsMoldInput() { + return this == MOLD_INPUT; + } + + /** 是否允许任何交互(非禁止) */ + public boolean allowsAny() { + return this != DISABLED; + } + + /** + * 循环到下一个模式。 + */ + public FurnaceFaceMode next() { + FurnaceFaceMode[] values = values(); + return values[(this.ordinal() + 1) % values.length]; + } + + public FurnaceFaceMode previous() { + FurnaceFaceMode[] values = values(); + return values[(this.ordinal() - 1 + values.length) % values.length]; + } + + public static FurnaceFaceMode byIndex(int index) { + FurnaceFaceMode[] values = values(); + if (index < 0 || index >= values.length) return DISABLED; + return values[index]; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/RarityExtension.java b/src/main/java/com/sorrowmist/useless/api/enums/RarityExtension.java new file mode 100644 index 00000000..be21b60c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/RarityExtension.java @@ -0,0 +1,12 @@ +package com.sorrowmist.useless.api.enums; + +import net.minecraft.ChatFormatting; +import net.minecraft.world.item.Rarity; + +/** Forge 1.20.1 extends Rarity through IExtensibleEnum, not NeoForge EnumProxy. */ +public final class RarityExtension { + public record Entry(Rarity value) { public Rarity getValue() { return value; } } + public static final Entry MYTHIC = new Entry(Rarity.create("USELESS_MYTHIC", ChatFormatting.GOLD)); + public static final Entry LEGENDARY = new Entry(Rarity.create("USELESS_LEGENDARY", ChatFormatting.RED)); + private RarityExtension() {} +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/RedstoneControlMode.java b/src/main/java/com/sorrowmist/useless/api/enums/RedstoneControlMode.java new file mode 100644 index 00000000..d4756c8c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/RedstoneControlMode.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.api.enums; + +/** + * 红石控制模式。 + *
+ * 控制合金炉是否响应红石信号来启用/禁用运行。 + */ +public enum RedstoneControlMode { + /** 无红石控制:始终运行 */ + DISABLED(172), + /** 高电平激活:有红石信号时运行 */ + HIGH_ACTIVE(156), + /** 低电平激活:无红石信号时运行 */ + LOW_ACTIVE(140); + + private final int overlayU; + + RedstoneControlMode(int overlayU) { + this.overlayU = overlayU; + } + + /** 获取UI覆盖图纹理U坐标(V=265),-1表示无覆盖 */ + public int getOverlayU() { + return this.overlayU; + } + + public boolean hasOverlay() { + return this.overlayU >= 0; + } + + public RedstoneControlMode next() { + RedstoneControlMode[] values = values(); + return values[(this.ordinal() + 1) % values.length]; + } + + public static RedstoneControlMode byIndex(int index) { + RedstoneControlMode[] values = values(); + if (index < 0 || index >= values.length) return DISABLED; + return values[index]; + } + + /** + * 判断给定红石信号强度下是否应该启用。 + * + * @param hasRedstoneSignal 是否有红石信号 + * @return 是否允许运行 + */ + public boolean shouldRun(boolean hasRedstoneSignal) { + return switch (this) { + case DISABLED -> true; + case HIGH_ACTIVE -> hasRedstoneSignal; + case LOW_ACTIVE -> !hasRedstoneSignal; + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/tool/ConstructionWandCoreMode.java b/src/main/java/com/sorrowmist/useless/api/enums/tool/ConstructionWandCoreMode.java new file mode 100644 index 00000000..a7c0fa9f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/tool/ConstructionWandCoreMode.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.api.enums.tool; + +import net.minecraft.network.chat.Component; + +public enum ConstructionWandCoreMode { + DEFAULT("tooltip.useless_mod.construction_wand_default_core"), + ANGEL("tooltip.useless_mod.construction_wand_angel_core"), + DESTRUCTION("tooltip.useless_mod.construction_wand_destruction_core"); + + private final String tooltipKey; + + ConstructionWandCoreMode(String tooltipKey) { + this.tooltipKey = tooltipKey; + } + + public Component getTooltip() { + return Component.translatable(this.tooltipKey); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/tool/EnchantMode.java b/src/main/java/com/sorrowmist/useless/api/enums/tool/EnchantMode.java new file mode 100644 index 00000000..01326f34 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/tool/EnchantMode.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.api.enums.tool; + +import net.minecraft.network.chat.Component; + +public enum EnchantMode { + SILK_TOUCH("silk_touch", "tooltip.useless_mod.silk_touch_mode"), + FORTUNE("fortune", "tooltip.useless_mod.fortune_mode"); + + private final String name; + private final String tooltipKey; + + EnchantMode(String name, String tooltipKey) { + this.name = name; + this.tooltipKey = tooltipKey; + } + + public static int getTotal() {return values().length;} + + public String getName() {return this.name;} + + public Component getTooltip() {return Component.translatable(this.tooltipKey);} +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/tool/ModeTypeEnum.java b/src/main/java/com/sorrowmist/useless/api/enums/tool/ModeTypeEnum.java new file mode 100644 index 00000000..709b7951 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/tool/ModeTypeEnum.java @@ -0,0 +1,271 @@ +package com.sorrowmist.useless.api.enums.tool; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public enum ModeTypeEnum { + // 增强连锁挖矿模式开关(true=启用增强连锁,false=使用普通连锁) + ENHANCED_CHAIN_MINING_ENABLED("enhanced_chain_mining_enabled", "tooltip.useless_mod.enhanced_chain_mining_mode"), + ENHANCED_CHAIN_MINING_DISABLED("enhanced_chain_mining_disabled", "tooltip.useless_mod.enhanced_chain_mining_mode"), + + // 强制挖掘模式 + FORCE_MINING_ENABLED("force_mining_enabled", "tooltip.useless_mod.force_mining_mode"), + FORCE_MINING_DISABLED("force_mining_disabled", "tooltip.useless_mod.force_mining_mode"), + + // 自动熔炼:true = 挖掘掉落物按熔炉配方熔炼后再入库 + AUTO_SMELT_ENABLED("auto_smelt_enabled", "tooltip.useless_mod.auto_smelt_mode"), + AUTO_SMELT_DISABLED("auto_smelt_disabled", "tooltip.useless_mod.auto_smelt_mode"), + + // AE存储优先模式 + AE_STORAGE_PRIORITY_ENABLED("ae_storage_priority_enabled", "tooltip.useless_mod.ae_storage_priority_mode"), + AE_STORAGE_PRIORITY_DISABLED("ae_storage_priority_disabled", "tooltip.useless_mod.ae_storage_priority_mode"), + + // AE 连接模式:右键有 AE 节点的机器,把它接入工具绑定的那张网 + AE_NETWORK_CONNECT_ENABLED("ae_network_connect_enabled", "tooltip.useless_mod.ae_network_connect_mode"), + AE_NETWORK_CONNECT_DISABLED("ae_network_connect_disabled", "tooltip.useless_mod.ae_network_connect_mode"), + + WRENCH_TAG_ENABLED("wrench_tag_enabled", "tooltip.useless_mod.wrench_tag_mode"), + WRENCH_TAG_DISABLED("wrench_tag_disabled", "tooltip.useless_mod.wrench_tag_mode"), + + CONSTRUCTION_WAND_ENABLED("construction_wand_enabled", "tooltip.useless_mod.construction_wand_mode"), + CONSTRUCTION_WAND_DISABLED("construction_wand_disabled", "tooltip.useless_mod.construction_wand_mode"), + CONSTRUCTION_WAND_ANGEL_CORE("construction_wand_angel_core", "tooltip.useless_mod.construction_wand_angel_core"), + CONSTRUCTION_WAND_DESTRUCTION_CORE("construction_wand_destruction_core", "tooltip.useless_mod.construction_wand_destruction_core"), + + FORCE_KILL("force_kill", "tooltip.useless_mod.force_kill_enabled_mode"), + + BEEF_MALUM_SPIRIT_ENABLED("beef_malum_spirit_enabled", "tooltip.useless_mod.beef_malum_spirit_mode"), + BEEF_MALUM_SPIRIT_DISABLED("beef_malum_spirit_disabled", "tooltip.useless_mod.beef_malum_spirit_mode"), + + BEEF_MYSTICAL_AGRICULTURE_ENABLED("beef_mystical_agriculture_enabled", + "tooltip.useless_mod.beef_mystical_agriculture_mode"), + BEEF_MYSTICAL_AGRICULTURE_DISABLED("beef_mystical_agriculture_disabled", + "tooltip.useless_mod.beef_mystical_agriculture_mode"), + + BEEF_BEHEADING_ENABLED("beef_beheading_enabled", "tooltip.useless_mod.beef_beheading_mode"), + BEEF_BEHEADING_DISABLED("beef_beheading_disabled", "tooltip.useless_mod.beef_beheading_mode"), + + BEEF_TIME_ACCELERATION_ENABLED("beef_time_acceleration_enabled", "tooltip.useless_mod.time_acceleration_mode"), + BEEF_TIME_ACCELERATION_DISABLED("beef_time_acceleration_disabled", "tooltip.useless_mod.time_acceleration_mode"), + + BEEF_INVULNERABILITY_ENABLED("beef_invulnerability_enabled", "tooltip.useless_mod.beef_invulnerability_mode"), + BEEF_INVULNERABILITY_DISABLED("beef_invulnerability_disabled", "tooltip.useless_mod.beef_invulnerability_mode"), + + BEEF_CAPTURE_ENABLED("beef_capture_enabled", "tooltip.useless_mod.beef_capture_mode"), + BEEF_CAPTURE_DISABLED("beef_capture_disabled", "tooltip.useless_mod.beef_capture_mode"), + + BEEF_TELEPORT_ENABLED("beef_teleport_enabled", "tooltip.useless_mod.beef_teleport_mode"), + BEEF_TELEPORT_DISABLED("beef_teleport_disabled", "tooltip.useless_mod.beef_teleport_mode"), + + BEEF_AOE_DAMAGE_ENABLED("beef_aoe_damage_enabled", "tooltip.useless_mod.beef_aoe_damage_mode"), + BEEF_AOE_DAMAGE_DISABLED("beef_aoe_damage_disabled", "tooltip.useless_mod.beef_aoe_damage_mode"), + + BEEF_MAGNET_ENABLED("beef_magnet_enabled", "tooltip.useless_mod.beef_magnet_mode"), + BEEF_MAGNET_DISABLED("beef_magnet_disabled", "tooltip.useless_mod.beef_magnet_mode"), + + BEEF_ADVANCED_STEALTH_ENABLED("beef_advanced_stealth_enabled", "tooltip.useless_mod.beef_advanced_stealth_mode"), + BEEF_ADVANCED_STEALTH_DISABLED("beef_advanced_stealth_disabled", "tooltip.useless_mod.beef_advanced_stealth_mode"), + + // 土壤右键模式:true = 锄头优先(耕地),false = 铲子优先(草径) + BEEF_FARMLAND_MODE_ENABLED("beef_farmland_mode_enabled", "tooltip.useless_mod.beef_farmland_mode"), + BEEF_FARMLAND_MODE_DISABLED("beef_farmland_mode_disabled", "tooltip.useless_mod.beef_farmland_mode"), + + // 「顺手收菜」:true = 右键成熟作物时收获并保留种子于耕地 + BEEF_CROP_HARVEST_ENABLED("beef_crop_harvest_enabled", "tooltip.useless_mod.beef_crop_harvest_mode"), + BEEF_CROP_HARVEST_DISABLED("beef_crop_harvest_disabled", "tooltip.useless_mod.beef_crop_harvest_mode"), + + // 剪刀功能:true = 造化杖可剪羊毛/剪掉落,并对外声明剪刀能力 + BEEF_SHEARS_ENABLED("beef_shears_enabled", "tooltip.useless_mod.beef_shears_mode"), + BEEF_SHEARS_DISABLED("beef_shears_disabled", "tooltip.useless_mod.beef_shears_mode"), + + // 打火石功能:true = 右键可点燃营火/蜡烛,或在可点火位置放火 + BEEF_FLINT_AND_STEEL_ENABLED("beef_flint_and_steel_enabled", "tooltip.useless_mod.beef_flint_and_steel_mode"), + BEEF_FLINT_AND_STEEL_DISABLED("beef_flint_and_steel_disabled", "tooltip.useless_mod.beef_flint_and_steel_mode"), + + // 匠心仪式挎包:true = 右键魔典预览的五芒星,从绑定的 AE 网络取方块摆出整座仪式 + BEEF_RITUAL_SATCHEL_ENABLED("beef_ritual_satchel_enabled", "tooltip.useless_mod.beef_ritual_satchel_mode"), + BEEF_RITUAL_SATCHEL_DISABLED("beef_ritual_satchel_disabled", "tooltip.useless_mod.beef_ritual_satchel_mode"), + + // 催熟:true = 右键可骨粉方块一键催到成熟,右键幼年动物直接催至成年 + BEEF_RIPEN_ENABLED("beef_ripen_enabled", "tooltip.useless_mod.beef_ripen_mode"), + BEEF_RIPEN_DISABLED("beef_ripen_disabled", "tooltip.useless_mod.beef_ripen_mode"), + + // 连点:true = 手持造化杖时以最快速度重复触发右键 + BEEF_AUTO_CLICK_ENABLED("beef_auto_click_enabled", "tooltip.useless_mod.beef_auto_click_mode"), + BEEF_AUTO_CLICK_DISABLED("beef_auto_click_disabled", "tooltip.useless_mod.beef_auto_click_mode"), + + // 强制生长:true = 催熟时额外用随机刻推进(可作用于甘蔗/仙人掌/竹子等骨粉无效的方块) + BEEF_FORCE_GROW_ENABLED("beef_force_grow_enabled", "tooltip.useless_mod.beef_force_grow_mode"), + BEEF_FORCE_GROW_DISABLED("beef_force_grow_disabled", "tooltip.useless_mod.beef_force_grow_mode"), + + // 无线物流:潜行右键容器绑定/解绑,界面里配置搬运规则 + BEEF_WIRELESS_LOGISTICS_ENABLED("beef_wireless_logistics_enabled", + "tooltip.useless_mod.beef_wireless_logistics_mode"), + BEEF_WIRELESS_LOGISTICS_DISABLED("beef_wireless_logistics_disabled", + "tooltip.useless_mod.beef_wireless_logistics_mode"), + + // Ex Deorum 钩子:true = 挖掘时按 exdeorum 的钩子配方追加掉落 + EXDEORUM_CROOK_ENABLED("exdeorum_crook_enabled", "tooltip.useless_mod.exdeorum_crook_mode"), + EXDEORUM_CROOK_DISABLED("exdeorum_crook_disabled", "tooltip.useless_mod.exdeorum_crook_mode"), + + // Ex Deorum 锤子:true = 挖掘时按 exdeorum 的锤子配方改写掉落 + EXDEORUM_HAMMER_ENABLED("exdeorum_hammer_enabled", "tooltip.useless_mod.exdeorum_hammer_mode"), + EXDEORUM_HAMMER_DISABLED("exdeorum_hammer_disabled", "tooltip.useless_mod.exdeorum_hammer_mode"), + + // Ex Deorum 压缩锤:配方体系与普通锤相互独立,单列一项开关 + EXDEORUM_COMPRESSED_HAMMER_ENABLED("exdeorum_compressed_hammer_enabled", + "tooltip.useless_mod.exdeorum_compressed_hammer_mode"), + EXDEORUM_COMPRESSED_HAMMER_DISABLED("exdeorum_compressed_hammer_disabled", + "tooltip.useless_mod.exdeorum_compressed_hammer_mode"), + + // 杀戮光环:手持造化杖每 20 tick 对「范围伤害」配置范围内生物结算一次攻击伤害 + BEEF_KILL_AURA_ENABLED("beef_kill_aura_enabled", "tooltip.useless_mod.beef_kill_aura_mode"), + BEEF_KILL_AURA_DISABLED("beef_kill_aura_disabled", "tooltip.useless_mod.beef_kill_aura_mode"), + + // 保护名单模式:开启时暂停光环与范围伤害,方便从容添加保护名单 + BEEF_PROTECT_MODE_ENABLED("beef_protect_mode_enabled", "tooltip.useless_mod.beef_protect_toggle_mode"), + BEEF_PROTECT_MODE_DISABLED("beef_protect_mode_disabled", "tooltip.useless_mod.beef_protect_toggle_mode"), + + // 生物加速:潜行右键生物加速其「计时器」(生长/繁殖冷却/下蛋/补货/长毛…),不跑 AI 与移动 + BEEF_ENTITY_TIME_ACCELERATION_ENABLED("beef_entity_time_acceleration_enabled", + "tooltip.useless_mod.beef_entity_time_acceleration_mode"), + BEEF_ENTITY_TIME_ACCELERATION_DISABLED("beef_entity_time_acceleration_disabled", + "tooltip.useless_mod.beef_entity_time_acceleration_mode"); + + private final String name; + private final String tooltipKey; + + ModeTypeEnum(String name, String tooltipKey) { + this.name = name; + this.tooltipKey = tooltipKey; + } + + public static int getTotal() {return values().length;} + + // 辅助方法:根据模式类型和状态获取对应的枚举值 + public static ModeTypeEnum getEnhancedChainMiningMode(boolean enabled) { + return enabled ? ENHANCED_CHAIN_MINING_ENABLED : ENHANCED_CHAIN_MINING_DISABLED; + } + + public static ModeTypeEnum getForceMiningMode(boolean enabled) { + return enabled ? FORCE_MINING_ENABLED : FORCE_MINING_DISABLED; + } + + public static ModeTypeEnum getAutoSmeltMode(boolean enabled) { + return enabled ? AUTO_SMELT_ENABLED : AUTO_SMELT_DISABLED; + } + + public static ModeTypeEnum getAEStoragePriorityMode(boolean enabled) { + return enabled ? AE_STORAGE_PRIORITY_ENABLED : AE_STORAGE_PRIORITY_DISABLED; + } + + public static ModeTypeEnum getAeNetworkConnectMode(boolean enabled) { + return enabled ? AE_NETWORK_CONNECT_ENABLED : AE_NETWORK_CONNECT_DISABLED; + } + + public static ModeTypeEnum getWrenchTagMode(boolean enabled) { + return enabled ? WRENCH_TAG_ENABLED : WRENCH_TAG_DISABLED; + } + + public static ModeTypeEnum getConstructionWandMode(boolean enabled) { + return enabled ? CONSTRUCTION_WAND_ENABLED : CONSTRUCTION_WAND_DISABLED; + } + + public static ModeTypeEnum getBeefInvulnerabilityMode(boolean enabled) { + return enabled ? BEEF_INVULNERABILITY_ENABLED : BEEF_INVULNERABILITY_DISABLED; + } + + public static ModeTypeEnum getBeefTimeAccelerationMode(boolean enabled) { + return enabled ? BEEF_TIME_ACCELERATION_ENABLED : BEEF_TIME_ACCELERATION_DISABLED; + } + + public static ModeTypeEnum getBeefMalumSpiritMode(boolean enabled) { + return enabled ? BEEF_MALUM_SPIRIT_ENABLED : BEEF_MALUM_SPIRIT_DISABLED; + } + + public static ModeTypeEnum getBeefMysticalAgricultureMode(boolean enabled) { + return enabled ? BEEF_MYSTICAL_AGRICULTURE_ENABLED : BEEF_MYSTICAL_AGRICULTURE_DISABLED; + } + + public static ModeTypeEnum getBeefBeheadingMode(boolean enabled) { + return enabled ? BEEF_BEHEADING_ENABLED : BEEF_BEHEADING_DISABLED; + } + + public static ModeTypeEnum getBeefCaptureMode(boolean enabled) { + return enabled ? BEEF_CAPTURE_ENABLED : BEEF_CAPTURE_DISABLED; + } + + public static ModeTypeEnum getBeefTeleportMode(boolean enabled) { + return enabled ? BEEF_TELEPORT_ENABLED : BEEF_TELEPORT_DISABLED; + } + + public static ModeTypeEnum getBeefAoeDamageMode(boolean enabled) { + return enabled ? BEEF_AOE_DAMAGE_ENABLED : BEEF_AOE_DAMAGE_DISABLED; + } + + public static ModeTypeEnum getBeefMagnetMode(boolean enabled) { + return enabled ? BEEF_MAGNET_ENABLED : BEEF_MAGNET_DISABLED; + } + + public static ModeTypeEnum getBeefAdvancedStealthMode(boolean enabled) { + return enabled ? BEEF_ADVANCED_STEALTH_ENABLED : BEEF_ADVANCED_STEALTH_DISABLED; + } + + public static ModeTypeEnum getBeefFarmlandMode(boolean enabled) { + return enabled ? BEEF_FARMLAND_MODE_ENABLED : BEEF_FARMLAND_MODE_DISABLED; + } + + public static ModeTypeEnum getBeefCropHarvestMode(boolean enabled) { + return enabled ? BEEF_CROP_HARVEST_ENABLED : BEEF_CROP_HARVEST_DISABLED; + } + + public static ModeTypeEnum getBeefShearsMode(boolean enabled) { + return enabled ? BEEF_SHEARS_ENABLED : BEEF_SHEARS_DISABLED; + } + + public static ModeTypeEnum getBeefFlintAndSteelMode(boolean enabled) { + return enabled ? BEEF_FLINT_AND_STEEL_ENABLED : BEEF_FLINT_AND_STEEL_DISABLED; + } + + public static ModeTypeEnum getBeefRitualSatchelMode(boolean enabled) { + return enabled ? BEEF_RITUAL_SATCHEL_ENABLED : BEEF_RITUAL_SATCHEL_DISABLED; + } + + public static ModeTypeEnum getBeefRipenMode(boolean enabled) { + return enabled ? BEEF_RIPEN_ENABLED : BEEF_RIPEN_DISABLED; + } + + public static ModeTypeEnum getBeefAutoClickMode(boolean enabled) { + return enabled ? BEEF_AUTO_CLICK_ENABLED : BEEF_AUTO_CLICK_DISABLED; + } + + public static ModeTypeEnum getBeefForceGrowMode(boolean enabled) { + return enabled ? BEEF_FORCE_GROW_ENABLED : BEEF_FORCE_GROW_DISABLED; + } + + public static ModeTypeEnum getBeefWirelessLogisticsMode(boolean enabled) { + return enabled ? BEEF_WIRELESS_LOGISTICS_ENABLED : BEEF_WIRELESS_LOGISTICS_DISABLED; + } + + public static ModeTypeEnum getBeefKillAuraMode(boolean enabled) { + return enabled ? BEEF_KILL_AURA_ENABLED : BEEF_KILL_AURA_DISABLED; + } + + public static ModeTypeEnum getBeefProtectMode(boolean enabled) { + return enabled ? BEEF_PROTECT_MODE_ENABLED : BEEF_PROTECT_MODE_DISABLED; + } + + public static ModeTypeEnum getBeefEntityTimeAccelerationMode(boolean enabled) { + return enabled ? BEEF_ENTITY_TIME_ACCELERATION_ENABLED : BEEF_ENTITY_TIME_ACCELERATION_DISABLED; + } + + public String getName() {return this.name;} + + public Component getTooltip() {return Component.translatable(this.tooltipKey);} + + public Component getStatusComponent(boolean active) { + return Component.translatable( + active ? "tooltip.useless_mod.enable" : "tooltip.useless_mod.disable" + ).withStyle(active ? ChatFormatting.GREEN : ChatFormatting.GRAY); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/tool/ToolTypeMode.java b/src/main/java/com/sorrowmist/useless/api/enums/tool/ToolTypeMode.java new file mode 100644 index 00000000..414ec3da --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/tool/ToolTypeMode.java @@ -0,0 +1,32 @@ +package com.sorrowmist.useless.api.enums.tool; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public enum ToolTypeMode { + NONE_MODE("none_mode", "tooltip.useless_mod.none_mode"), + WRENCH_MODE("wrench_mode", "tooltip.useless_mod.wrench_mode"), + SCREWDRIVER_MODE("screwdriver_mode", "tooltip.useless_mod.screwdriver_mode"), + MALLET_MODE("mallet_mode", "tooltip.useless_mod.mallet_mode"), + CROWBAR_MODE("crowbar_mode", "tooltip.useless_mod.crowbar_mode"), + HAMMER_MODE("hammer_mode", "tooltip.useless_mod.hammer_mode"), + OMNITOOL_MODE("omnitool_mode", "tooltip.useless_mod.omnitool_mode"); + + private final String name; + private final String tooltipKey; + + ToolTypeMode(String name, String tooltipKey) { + this.name = name; + this.tooltipKey = tooltipKey; + } + + public static int getTotal() {return values().length;} + + public String getName() {return this.name;} + + public Component getTooltip() {return Component.translatable(this.tooltipKey);} + + public Component getTooltip(ChatFormatting... styles) { + return Component.translatable(this.tooltipKey).withStyle(styles); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongEnergyHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongEnergyHandler.java new file mode 100644 index 00000000..286636b7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongEnergyHandler.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.api.logistics; + +/** + * long 级能量处理器。 + * + *
MinecraftForge 的 {@code IEnergyStorage} 只有 int 面。本模组自己的 {@code IEnergyManager} + * 从设计之初就是 long 契约,因此会原生长途直通:不存在任何 int 中转,也没有补循环。
+ * + *外部模组的 int 级能量能力由适配器包装成这一层,内部分片细节被封在适配器里, + * 调用方看不到也不需要知道。
+ */ +public interface LongEnergyHandler extends LongResourceHandler { + + /** 抽出最多 {@code amount} 能量,返回实际抽出量。 */ + long extract(long amount, boolean simulate); + + /** 接收最多 {@code amount} 能量,返回实际接收量。 */ + long receive(long amount, boolean simulate); + + /** 当前储量。 */ + long stored(); + + /** 容量。 */ + long capacity(); + + default boolean canExtract() { + return true; + } + + default boolean canReceive() { + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongFluidHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongFluidHandler.java new file mode 100644 index 00000000..91bd6bb0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongFluidHandler.java @@ -0,0 +1,92 @@ +package com.sorrowmist.useless.api.logistics; + +import net.minecraftforge.fluids.FluidStack; + +/** + * long 级流体处理器。 + * + *MinecraftForge 的 {@code IFluidHandler} 单次只能 {@code fill}/{@code drain} 一个 int, + * 且 {@code FluidStack} 的 amount 本身就是 int。这里把「类型」和「数量」拆开表达: + * 数量走 {@code long} 参数与返回值,类型由 {@link FluidStack} 单独承载(其 amount 视为占位)。
+ */ +public interface LongFluidHandler extends LongResourceHandler { + + /** 储罐数量。 */ + int getTanks(); + + /** + * 查看某个储罐里的流体类型。只读:返回栈的 amount 不代表罐内真实数量, + * 真实数量请用 {@link #amountIn(int)}。 + */ + FluidStack getFluidInTank(int tank); + + /** 储罐内实际流体量(long 语义)。空罐返回 0。 */ + long amountIn(int tank); + + /** 储罐容量(long 语义)。 */ + long capacityOf(int tank); + + /** + * 该处理器里一共有多少这种流体(跨罐累加,long 语义)。 + * + *语义与 {@link LongItemHandler#amountOf(net.minecraft.world.item.ItemStack)} 完全对称: + * 给过滤器的「源端保留」「接收端上限」提供「现在有多少」这个基准。
+ * + *默认实现 = {@link #findTank(FluidStack)} + {@link #amountIn(int)}; + * AE 端点必须覆写(否则要抓整网快照,且按类型跨罐累加才算得对)。
+ */ + default long amountOf(FluidStack type) { + if (type == null || type.isEmpty()) { + return 0L; + } + int tank = findTank(type); + return tank < 0 ? 0L : amountIn(tank); + } + + /** 从指定储罐抽出 {@code amount}。返回值是实际抽出量;{@code simulate} 为真时只试算。 */ + long drain(int tank, long amount, boolean simulate); + + /** + * 向该处理器注入 {@code type} 流体 {@code amount}。 + * {@code type} 只取其流体类型与组件,其 amount 被忽略。 + * + * @return 实际注入量;{@code simulate} 为真时只试算 + */ + long fill(FluidStack type, long amount, boolean simulate); + + /** + * 找出「和给定流体同类型」的储罐;找不到返回 -1。 + * + *理由同 {@link LongItemHandler#findSlot(net.minecraft.world.item.ItemStack)}: + * 过滤器驱动时按类型直接定位,不要靠扫描——AE 端点的 {@code getTanks()} 是网络里的 + * 流体种类数,扫描会被调用方的预算截断,排在后面的那些永远找不到。
+ */ + default int findTank(FluidStack type) { + if (type == null || type.isEmpty()) { + return -1; + } + for (int tank = 0; tank < getTanks(); tank++) { + if (amountIn(tank) > 0L + && com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(getFluidInTank(tank), type)) { + return tank; + } + } + return -1; + } + + /** + * 按类型直接抽走 {@code amount},不经过「第几号罐」。语义与 + * {@link LongItemHandler#extractMatching(net.minecraft.world.item.ItemStack, long, boolean)} + * 完全对称:一次调用同时回答「有没有这一种」和「能抽多少」,默认实现 = + * {@link #findTank(FluidStack)} + {@link #drain(int, long, boolean)}。 + * + *AE 端点必须覆写它 —— 否则每次都会先抓一份整网快照(遍历 ME 网络里的每一种流体)。
+ */ + default long drainMatching(FluidStack type, long amount, boolean simulate) { + if (type == null || type.isEmpty() || amount <= 0L) { + return 0L; + } + int tank = findTank(type); + return tank < 0 ? 0L : drain(tank, amount, simulate); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongItemHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongItemHandler.java new file mode 100644 index 00000000..f6a08ee3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongItemHandler.java @@ -0,0 +1,151 @@ +package com.sorrowmist.useless.api.logistics; + +import net.minecraft.world.item.ItemStack; + +/** + * long 级物品处理器。 + * + *和 MinecraftForge 的 {@code IItemHandler} 相比,这里的「数量」参数与返回值都是 {@code long}: + * 一次调用要搬的数量不会被截断到 {@link Integer#MAX_VALUE},调用方也不需要在外面补循环。
+ * + *槽内堆叠上限仍然是物理事实(原版 64),所以底层是 int 槽位的实现一次能抽出的数量受堆叠 + * 限制;但接口本身不设 int 上限,超大容量或多槽聚合的实现可以一次性给出更多。
+ */ +public interface LongItemHandler extends LongResourceHandler { + + /** 槽位数。 */ + int getSlots(); + + /** + * 查看槽里的物品。只读:返回的栈仅用于判断类型,其 count 不代表槽内真实数量, + * 真实数量请用 {@link #amountIn(int)}。 + */ + ItemStack getStackInSlot(int slot); + + /** + * 扫描路径专用的读取入口:只用于判断类型,数量看 {@link #amountIn(int)}。 + * + *与 {@link #getStackInSlot(int)} 的唯一区别是返回值不保证是副本——实现可以直接 + * 交出自己内部的对象。因此调用方不得长期持有它,也不得在 extract 之后依赖它: + * 槽位可能被就地清空,手里那个对象会跟着一起变空(典型是原生 {@code IItemHandler} 的 + * 引用型返回值)。需要在抽取之后继续用的话,先自己 {@code copy()} 一份。
+ * + *存在的理由:有的实现(例如 AE2 的 {@code GenericStackItemStorage})每次 + * {@code getStackInSlot} 都新建一个 {@code ItemStack},外层再 copy 一遍纯属浪费—— + * 在 1.21 的组件化物品栈上,一次 copy 要连带复制整份 DataComponent 映射,实测占无线物流 + * 总耗时的 13%(见内部无线物流性能分析报告第十二节)。
+ * + *默认实现委托 {@link #getStackInSlot(int)},因此旧实现的行为逐字节不变。
+ */ + default ItemStack peekStack(int slot) { + return getStackInSlot(slot); + } + + /** 槽内实际数量(long 语义)。空槽返回 0。 */ + long amountIn(int slot); + + /** + * 扫描路径的优化入口:调用方手里已经有 {@link #getStackInSlot(int)} 的结果。 + * + *默认实现忽略 {@code stackFromSlot},直接走 {@link #amountIn(int)}——这是正确的 + * 默认值,因为 {@link #getStackInSlot(int)} 的返回值「count 不代表槽内真实数量」(见其说明)。 + * 只有「槽内栈的 count 就是真实数量」的实现才该覆写它。
+ * + *存在的理由:扫描路径本来就要读一次 {@code getStackInSlot} 拿类型,紧接着又要 + * {@code amountIn} 拿数量。对底层是 int 槽位的实现,后者等于把同一个槽位再读一遍; + * 对 AE 端点则是一次完整的 {@code AEKey → ItemStack} 物化(要连带复制整份组件映射)。 + * 覆写它就能把这一次省掉。
+ * + *调用方必须保证 {@code stackFromSlot} 确实取自同一个 {@code slot}: + * 覆写实现会直接拿它的 count 当答案。
+ */ + default long amountIn(int slot, ItemStack stackFromSlot) { + return amountIn(slot); + } + + /** + * 该处理器里一共有多少这种物品(跨槽累加,long 语义)。 + * + *存在的理由:过滤器的「源端保留 X 个」「接收端最多 Y 个」需要知道「现在有多少」, + * 而这既不是某个槽位的数字(可能分散在多个槽),也不是 {@code extract(..., simulate)} + * 的返回值(那个还叠了容量上限)。
+ * + *默认实现 = {@link #findSlot(ItemStack)} + {@link #amountIn(int)},只回答「第一个 + * 同类型槽位的存量」。对「同一种物品最多出现在一个槽位」的实现(原版容器、本模组的 + * 大容量容器)这已经是正确答案;对可能分散的实现应覆写。
+ * + *调用方只在真的需要限制时才调它:AE 端点覆写后要抓一次整网快照, + * 那是线性开销,不能当成免费查询。
+ */ + default long amountOf(ItemStack template) { + if (template == null || template.isEmpty()) { + return 0L; + } + int slot = findSlot(template); + return slot < 0 ? 0L : amountIn(slot); + } + + /** + * 从指定槽位抽走 {@code amount} 个物品。 + * + * @return 实际抽出的数量(可能小于请求量);{@code simulate} 为真时只试算不改动 + */ + long extract(int slot, long amount, boolean simulate); + + /** + * 插入物品。{@code template} 只取其类型,其 count 被忽略,实际插入量由 {@code amount} 决定。 + * + * @return 实际插入的数量(可能小于请求量);{@code simulate} 为真时只试算不改动 + */ + long insert(ItemStack template, long amount, boolean simulate); + + /** + * 找出「和给定物品同类型」的槽位;找不到返回 -1。 + * + *过滤器驱动的搬运靠它直接定位:有过滤器时「要搬什么」已经完全确定,直接问 + * 「源端有没有这一种」即可,不必扫全表。
+ * + *对条目数很大的实现必须覆写。默认实现是线性扫描,而 AE 端点的 + * {@link #getSlots()} 是整个 ME 网络的资源种类数(动辄上千),靠扫描找某一种既慢, + * 又会因为调用方的扫描预算而永远找不到排在后面的那些。AE 端点按资源键定位。
+ */ + default int findSlot(ItemStack template) { + if (template == null || template.isEmpty()) { + return -1; + } + for (int slot = 0; slot < getSlots(); slot++) { + if (amountIn(slot) > 0L + && ItemStack.isSameItemSameTags(getStackInSlot(slot), template)) { + return slot; + } + } + return -1; + } + + /** + * 按类型直接抽走 {@code amount} 个 {@code template},不经过「第几号槽」。 + * + *过滤器驱动的搬运只走这一个入口。它把「源端有没有这一种」和「能抽多少」 + * 合并成一次调用({@code simulate} 为真时返回值就是可抽量),调用方因此不必先 + * {@link #findSlot(ItemStack)} 探一次、再 {@code amountIn} 问一次。
+ * + *默认实现 = {@link #findSlot(ItemStack)} + {@link #extract(int, long, boolean)}, + * 对槽位有限的实现完全够用(多一次槽位扫描,代价可忽略)。
+ * + *条目数很大的实现必须覆写。默认实现要先 {@code findSlot},而 AE 端点的 + * {@code findSlot} 需要先抓一份整网快照 —— 那是一次完整的 + * {@code MEStorage.getAvailableStacks()},把 ME 网络里的每一种资源都遍历一遍。 + * 覆写成「拿 {@code template} 直接构造资源键、问一次 {@code MEStorage.extract}」即可 + * 把这一步变成一次哈希查找。AE2 的 {@code extract} 本身就是 long 签名, + * 「先模拟后提交」的时序也完全不变。
+ * + * @return 实际抽出的数量;{@code simulate} 为真时只试算不改动 + */ + default long extractMatching(ItemStack template, long amount, boolean simulate) { + if (template == null || template.isEmpty() || amount <= 0L) { + return 0L; + } + int slot = findSlot(template); + return slot < 0 ? 0L : extract(slot, amount, simulate); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongPressureHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongPressureHandler.java new file mode 100644 index 00000000..0a7b7eb7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongPressureHandler.java @@ -0,0 +1,62 @@ +package com.sorrowmist.useless.api.logistics; + +/** + * long 级气压处理器。 + * + *与 {@link LongEnergyHandler} 同一层契约,但多带两个「气压」特有的量:{@link #volume()} 与 + * {@link #pressure()}。气压与能量、流体都不同:它是状态量而不是纯流量——「搬多少」取决于 + * 目标离目标气压还差多少,而不是每轮固定搬一批。所以端点必须能报出自己的体积与当前空气量, + * 调用方才算得出「要补多少 / 要抽多少」。
+ * + *空气量可以为负。 气动工艺用 {@code air = pressure × volume} 表示空气量,真空就是负值 + * (下限 {@code -volume},即 -1 bar 的绝对真空)。因此 {@link #stored()} 是有符号的, + * {@link #capacity()} 只表示正压侧的上限。
+ */ +public interface LongPressureHandler extends LongResourceHandler { + + /** + * 移除最多 {@code amount} mL 空气,返回实际移除量。 + * + *下限是 0,不制造真空。 这条是「把源端的气搬到别处」用的——抽到 0 bar(无压)为止, + * 不会把源端抽成负压。要主动降成真空请用 {@link #setAirTo}。
+ */ + long extract(long amount, boolean simulate); + + /** 接收最多 {@code amount} mL 空气,返回实际接收量。 */ + long receive(long amount, boolean simulate); + + /** 当前空气量(mL)。可为负:负值表示真空。 */ + long stored(); + + /** + * 正压侧容量(mL);AE 网络没有上限,取 {@link Long#MAX_VALUE}。 + * + *方块侧取的是「危险压力 × 体积」——即开始有爆炸风险的那个压力对应的空气量, + * 而不是实现自己报的某个常量上限(气动的 {@code maxPressure()} 恒为 10 bar,不反映真实阈值)。
+ */ + long capacity(); + + /** + * 有效体积(mL)。 + * + *AE 网络这类「只有一堆空气、没有容器」的承载返回 {@code 0}——它没有压力概念, + * 因此目标气压对它无意义。
+ */ + long volume(); + + /** 当前压力(bar);无体积的承载恒为 0。 */ + default double pressure() { + long volume = volume(); + return volume > 0L ? (double) stored() / (double) volume : 0.0D; + } + + /** + * 把空气量直接调整到 {@code targetAir}(mL,可为负 = 真空),返回实际变化量。 + * + *供「把多余空气排到环境」这类没有源端的调节使用:降真空时空气量会越过 0 继续下降, + * 因此不能用 {@link #extract}。无体积的承载(AE 网络)不支持该操作,返回 0。
+ */ + default long setAirTo(long targetAir, boolean simulate) { + return 0L; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongResourceHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongResourceHandler.java new file mode 100644 index 00000000..9dc8ca23 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongResourceHandler.java @@ -0,0 +1,13 @@ +package com.sorrowmist.useless.api.logistics; + +/** + * long 级资源处理器的公共标记。 + * + *{@link LongItemHandler}、{@link LongFluidHandler}、{@link LongEnergyHandler} 都实现它, + * 让「解析出来的处理器」有一个统一的静态类型,调用方不必先 cast 成 {@code Object} 再试。
+ * + *化学品刻意不在这个家族里:它的视图接口({@code ChemicalHandlerView})从设计之初就以 long + * 计数,本身已经是 long 级契约,再包一层只会多一次无意义的转发。
+ */ +public interface LongResourceHandler { +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/package-info.java b/src/main/java/com/sorrowmist/useless/api/logistics/package-info.java new file mode 100644 index 00000000..06fe45ad --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/package-info.java @@ -0,0 +1,13 @@ +/** + * long 级资源搬运接口。 + * + *MinecraftForge 原生的槽位能力({@code IItemHandler} / {@code IFluidHandler} / + * {@code IEnergyStorage})每一步都收 {@code int}。于是「一次搬 5×10^9 个」这种请求会被先截断成 + * {@link Integer#MAX_VALUE},剩下的只能靠调用方在外面反复循环补足——既慢,又让调用方必须自己 + * 维护「还剩多少没搬」的状态。
+ * + *这一层把 long 语义提到接口本身:调用方说一次「最多搬这么多」,由实现决定用多少次底层 + * {@code int} 调用把它凑出来。本模组自己的 long 能力(如 {@code IEnergyManager})会被直接识别并 + * 原生长途直通,不再经过任何 int 中转。
+ */ +package com.sorrowmist.useless.api.logistics; diff --git a/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceCompatApi.java b/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceCompatApi.java new file mode 100644 index 00000000..a2a407ab --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceCompatApi.java @@ -0,0 +1,76 @@ +package com.sorrowmist.useless.api.recipe; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import org.slf4j.Logger; + +import java.util.Objects; +import java.util.function.BooleanSupplier; + +/** + * Entry point for third-party alloy-furnace compatibility modules. + * + *{@code
+ * event.enqueueWork(() -> AlloyFurnaceCompatApi.register(
+ * "example_compat", Config.ENABLED::get, new ExampleRecipeAdapter()));
+ * }
+ */
+public final class AlloyFurnaceCompatApi {
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ private AlloyFurnaceCompatApi() {
+ }
+
+ /** Registers adapters with a source that is always enabled. */
+ public static void register(String sourceId, IRecipeAdapter>... adapters) {
+ register(sourceId, () -> true, adapters);
+ }
+
+ /**
+ * Registers adapters when {@code enabled} is true.
+ *
+ * The supplier is evaluated once during registration. Configuration changes should take + * effect after the normal restart required by the owning mod's configuration.
+ */ + public static void register(String sourceId, BooleanSupplier enabled, + IRecipeAdapter>... adapters) { + Objects.requireNonNull(sourceId, "sourceId"); + Objects.requireNonNull(enabled, "enabled"); + if (sourceId.isBlank()) { + throw new IllegalArgumentException("sourceId cannot be blank"); + } + if (adapters == null || adapters.length == 0) { + return; + } + + final boolean isEnabled; + try { + isEnabled = enabled.getAsBoolean(); + } catch (RuntimeException exception) { + RateLimitedLogger.error(LOGGER, "Failed to evaluate alloy-furnace compatibility switch: source={}", + sourceId, exception); + return; + } + if (!isEnabled) { + return; + } + + String normalizedSource = RecipeSourceIds.normalize(sourceId); + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + for (IRecipeAdapter> adapter : adapters) { + if (adapter == null) { + RateLimitedLogger.warn(LOGGER, "Skipping null alloy-furnace compatibility adapter: source={}", normalizedSource); + continue; + } + try { + manager.registerAdapter(adapter, normalizedSource); + } catch (RuntimeException | LinkageError exception) { + RateLimitedLogger.error(LOGGER, "Failed to register alloy-furnace compatibility adapter: source={}, adapter={}", + normalizedSource, adapter.getClass().getName(), exception); + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceRecipeBuilder.java b/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceRecipeBuilder.java new file mode 100644 index 00000000..b6331165 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceRecipeBuilder.java @@ -0,0 +1,138 @@ +package com.sorrowmist.useless.api.recipe; + +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipeBuilder; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; + +import java.util.List; + +/** Public builder for recipes returned by an alloy-furnace compatibility adapter. */ +public final class AlloyFurnaceRecipeBuilder extends AdvancedAlloyFurnaceRecipeBuilder { + private AlloyFurnaceRecipeBuilder() { + } + + public static AlloyFurnaceRecipeBuilder create() { + return new AlloyFurnaceRecipeBuilder(); + } + + @Override + public AlloyFurnaceRecipeBuilder input(Ingredient ingredient, long count) { + super.input(ingredient, count); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder input(ItemLike item, long count) { + super.input(item, count); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder fluidInput(FluidStack fluid) { + super.fluidInput(fluid); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder fluidInput(SizedFluidIngredient fluid) { + super.fluidInput(fluid); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder fluidInput(LongSizedFluidIngredient fluid) { + super.fluidInput(fluid); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder keyInput(GenericStack keyInput) { + super.keyInput(keyInput); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder output(ItemLike item, int count) { + super.output(item, count); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder output(ItemStack output) { + super.output(output); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder fluidOutput(FluidStack output) { + super.fluidOutput(output); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder keyOutput(GenericStack keyOutput) { + super.keyOutput(keyOutput); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder energy(long energy) { + super.energy(energy); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder processTime(int ticks) { + super.processTime(ticks); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder catalyst(Ingredient catalyst, int uses) { + super.catalyst(catalyst, uses); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder mold(Ingredient mold) { + super.mold(mold); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder molds(ListThe manager supplies merged inputs to the matching methods. Adapters should keep the + * ingredient and component semantics represented by those maps instead of reducing them to a + * representative stack.
+ * + * @param开关是客户端会话级状态({@link #isEnabled()}),不绑定任何物品 —— 空手或拿着 + * 任意物品都能开启,开启后每个客户端 tick 按配置次数重复触发右键。判定顺序完全复刻 + * {@code Minecraft#startUseItem}:先主手、后副手,每只手先看视线命中(实体 → 方块), + * 命中被消费就结束这一次;都没消费时才兜底走物品自身的 {@code use}(空手没有 {@code use},跳过)。 + * 这样连点触发的是真正的右键交互,造化杖自己的行为链、以及其它模组的 + * 右键功能都会照常生效(例如同时开启催熟模式即可自动反复催熟)。
+ * + *速率由客户端配置 {@code beef_autoclick_clicks_per_tick} 控制(默认 4 次/tick)。 + * 之所以不复用原版的 {@code rightClickDelay},是因为它会把速率压到约 4 次/秒, + * 达不到「尽可能快」的要求。
+ */ +public final class BeefAutoClicker { + + /** 与 {@code Minecraft#startUseItem} 一致的尝试顺序:主手 → 副手。 */ + private static final InteractionHand[] HANDS = InteractionHand.values(); + + /** 连点开关:客户端会话级,登录时由 {@code ClientEventBusSubscriber} 重置为关。 */ + private static volatile boolean enabled; + + private BeefAutoClicker() { + } + + public static boolean isEnabled() { + return enabled; + } + + public static void setEnabled(boolean value) { + enabled = value; + } + + /** 翻转开关并返回新状态。 */ + public static boolean toggle() { + enabled = !enabled; + return enabled; + } + + /** 登录 / 断线时把开关重置为关,避免状态跨会话残留。 */ + public static void reset() { + enabled = false; + } + + /** 客户端 tick 入口。 */ + public static void tick(Minecraft mc) { + if (!enabled) { + return; + } + LocalPlayer player = mc.player; + if (player == null || mc.level == null || mc.gameMode == null) { + return; + } + // 开着界面 / 游戏暂停时不连点,避免误操作 + if (mc.screen != null || mc.isPaused()) { + return; + } + // 正在吃东西/拉弓/用刷子,或正在挖方块时让位给玩家自己 + if (player.isUsingItem() || player.isHandsBusy() || mc.gameMode.isDestroying()) { + return; + } + + int clicks = ConfigManager.getBeefAutoClickClicksPerTick(); + for (int i = 0; i < clicks; i++) { + if (!performRightClick(mc, player)) { + break; + } + } + } + + /** + * 执行一次右键交互。 + * + *与 {@code Minecraft#startUseItem} 一致:依次尝试主手、副手。每只手先看视线命中 + * (实体走 {@code interact}、方块走 {@code useItemOn}),命中被消费就结束;都没消费时才兜底走 + * 该手物品自身的 {@code use}。空手不调用 {@code use}(原版亦然),但空手依然会触发 + * 方块 / 实体的交互 —— 这正是「空手连点」的意义所在。
+ * + *副手一定要试:主手空着、副手拿着物品时,若只走主手就完全不会用到副手那件物品 + * (2026-10-07 用户报的「副手持有物品开启连点没生效」)。
+ * + * @return 本次是否真的触发了一次交互(false 表示当前状态不适合连点,应停止本 tick 的循环) + */ + private static boolean performRightClick(Minecraft mc, LocalPlayer player) { + for (InteractionHand hand : HANDS) { + ItemStack stack = player.getItemInHand(hand); + + HitResult hit = mc.hitResult; + if (hit != null) { + // 1. 命中实体:优先交给实体交互 + if (hit.getType() == HitResult.Type.ENTITY && hit instanceof EntityHitResult entityHit) { + Entity entity = entityHit.getEntity(); + if (!mc.level.getWorldBorder().isWithinBounds(entity.blockPosition())) { + return false; + } + InteractionResult result = mc.gameMode.interact(player, entity, hand); + if (result.consumesAction()) { + player.swing(hand); + return true; + } + } else if (hit.getType() == HitResult.Type.BLOCK && hit instanceof BlockHitResult blockHit) { + // 2. 命中方块:空气方块不触发(与原版一致) + BlockState state = mc.level.getBlockState(blockHit.getBlockPos()); + if (!state.isAir()) { + InteractionResult result = mc.gameMode.useItemOn(player, hand, blockHit); + if (result.consumesAction()) { + player.swing(hand); + return true; + } + } + } + } + + // 3. 兜底:走该手物品自身的 use(例如对空气右键、或方块交互未消费时)。空手没有 use 可用。 + if (!stack.isEmpty()) { + InteractionResult result = mc.gameMode.useItem(player, hand); + if (result.consumesAction()) { + player.swing(hand); + return true; + } + } + } + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/BeefTooltipKeyHandler.java b/src/main/java/com/sorrowmist/useless/client/BeefTooltipKeyHandler.java new file mode 100644 index 00000000..e115911d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/BeefTooltipKeyHandler.java @@ -0,0 +1,54 @@ +package com.sorrowmist.useless.client; + +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.InputEvent; +import org.lwjgl.glfw.GLFW; + +/** + * 造化杖提示翻页的按键入口。 + * + *翻页键 {@code [} 与 {@code ]} 不注册为 {@code KeyMapping}:它们只在物品提示框 + * 显示期间生效,注册为常规绑定会被原版按键系统在游戏内持续分发,而并不存在对应的游戏内行为。
+ * + *{@link InputEvent.Key} 不提供取消机制,因此按键仍会继续传递。这两个键在游戏中未绑定 + * 任何行为,且分页只在提示框可见时改写页码,无提示框时处理逻辑提前返回,故不产生副作用。
+ */ +@OnlyIn(Dist.CLIENT) +@EventBusSubscriber(Dist.CLIENT) +public final class BeefTooltipKeyHandler { + + private BeefTooltipKeyHandler() { + } + + @SubscribeEvent + public static void onKey(InputEvent.Key event) { + if (event.getAction() != GLFW.GLFW_PRESS) return; + + int delta; + if (event.getKey() == GLFW.GLFW_KEY_RIGHT_BRACKET) { + delta = 1; + } else if (event.getKey() == GLFW.GLFW_KEY_LEFT_BRACKET) { + delta = -1; + } else { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || !TooltipPageState.isTooltipVisible()) return; + + // 必须使用提示框渲染时记录的那个物品栈,而不是重新按主手/副手推断: + // 在物品栏中悬停时,被查看的是槽位内的物品栈实例,与快捷栏的 ItemStack + // 并非同一对象,两者的标识哈希不同。若按键阶段改用主手物品翻页,页码会写到 + // 渲染阶段从不读取的键上,表现为按键无效。 + ItemStack staff = TooltipPageState.getViewedStack(); + if (staff == null || !(staff.getItem() instanceof EndlessBeafItem)) return; + + BeefTooltipPager.turnPage(staff, delta); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/BeefTooltipPager.java b/src/main/java/com/sorrowmist/useless/client/BeefTooltipPager.java new file mode 100644 index 00000000..b8486a28 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/BeefTooltipPager.java @@ -0,0 +1,82 @@ +package com.sorrowmist.useless.client; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +/** + * 造化杖提示分页状态。 + * + *造化杖的提示行数已超过屏幕可用高度,超出部分在原版提示框中被裁掉。此处把提示内容 + * 按页切分:渲染时只取当前页对应的行,玩家用 {@code [} 与 {@code ]} 翻页。
+ * + *页码按物品栈缓存(键为 {@link System#identityHashCode(Object)}),而非全局单值: + * 切换所持物品、物品被消耗或切换存档后,原键不再出现,页码自然回到首页,无需在事件中清理。
+ */ +@OnlyIn(Dist.CLIENT) +public final class BeefTooltipPager { + + /** 单页最多行数。 */ + public static final int LINES_PER_PAGE = 12; + + private BeefTooltipPager() { + } + + /** 读取当前物品栈的页码(从 0 开始)。 */ + public static int getPage(ItemStack stack) { + return TooltipPageState.pageOf(identityOf(stack)); + } + + /** + * 取物品栈的标识哈希。 + * + *{@link ItemStack} 未覆写 {@code equals} 与 {@code hashCode},其默认实现即按对象身份比较, + * 因此直接沿用 {@link System#identityHashCode(Object)}。该值在一次游戏会话内唯一且不复用。
+ */ + private static int identityOf(ItemStack stack) { + return System.identityHashCode(stack); + } + + /** + * 翻页。 + * + * @param delta 正值向后翻页,负值向前翻页 + */ + public static void turnPage(ItemStack stack, int delta) { + TooltipPageState.turnPage(identityOf(stack), delta); + } + + /** 把行数换算成总页数,至少为 1。 */ + public static int pageCount(int lineCount) { + return Math.max(1, (lineCount + LINES_PER_PAGE - 1) / LINES_PER_PAGE); + } + + /** + * 裁取当前页对应的行区间,并把回绕后的页码写回存储。 + * + *页码在按键阶段只能单调累加,真正的回绕需要总行数,因此推迟到渲染阶段在此完成。 + * 连续按同一方向键时页码可无界增长,取模后等价于循环翻页。
+ * + * @return 长度恰为 2 的数组 {@code [fromIndex, toIndex)},可直接用于 {@code List#subList} + */ + public static int[] pageRange(ItemStack stack, int lineCount, int totalPages) { + int page = Math.floorMod(getPage(stack), totalPages); + TooltipPageState.setPage(identityOf(stack), page); + int from = Math.min(page * LINES_PER_PAGE, lineCount); + int to = Math.min(from + LINES_PER_PAGE, lineCount); + return new int[]{from, to}; + } + + /** 生成翻页指示行,形如 {@code 1/3 [ ] 翻页}。 */ + public static MutableComponent pageIndicator(int page, int totalPages) { + return Component.translatable( + "tooltip.useless_mod.tooltip_page", + page + 1, + totalPages, + "[", + "]" + ); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/ClientEventBusSubscriber.java b/src/main/java/com/sorrowmist/useless/client/ClientEventBusSubscriber.java deleted file mode 100644 index c237443c..00000000 --- a/src/main/java/com/sorrowmist/useless/client/ClientEventBusSubscriber.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.sorrowmist.useless.networking.ChainMiningTogglePacket; -import com.sorrowmist.useless.networking.EnchantmentSwitchPacket; -import com.sorrowmist.useless.networking.ModMessages; -import com.sorrowmist.useless.networking.TriggerForceMiningPacket; -import net.minecraft.client.Minecraft; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.client.event.InputEvent; -import net.minecraftforge.client.event.RegisterKeyMappingsEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; - -/** - * 客户端事件订阅器 - * 重构:ForceBreakKey发送时检查Tab键状态 - */ -@Mod.EventBusSubscriber(modid = UselessMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) -public class ClientEventBusSubscriber { - - @SubscribeEvent - public static void onKeyInput(InputEvent.Key event) { - Minecraft minecraft = Minecraft.getInstance(); - // 检查玩家是否打开了GUI或正在使用物品 - if (minecraft.screen != null) { - return; - } - - if (KeyBindings.SWITCH_SILK_TOUCH_KEY.consumeClick()) { - if (minecraft.player != null) { - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - ModMessages.sendToServer(new EnchantmentSwitchPacket(true)); - } - } - } - - if (KeyBindings.SWITCH_FORTUNE_KEY.consumeClick()) { - if (minecraft.player != null) { - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - ModMessages.sendToServer(new EnchantmentSwitchPacket(false)); - } - } - } - - // 处理连锁挖掘按键的按下和松开事件 - if (KeyBindings.SWITCH_CHAIN_MINING_KEY.isDown() != KeyBindings.SWITCH_CHAIN_MINING_KEY_WAS_DOWN) { - KeyBindings.SWITCH_CHAIN_MINING_KEY_WAS_DOWN = KeyBindings.SWITCH_CHAIN_MINING_KEY.isDown(); - if (minecraft.player != null) { - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - UselessMod.NETWORK.sendToServer(new ChainMiningTogglePacket(KeyBindings.SWITCH_CHAIN_MINING_KEY.isDown())); - } - } - } - - // 处理强制挖掘触发按键(R键)- 重构:传入Tab键状态以支持R+Tab连锁强制破坏 - if (KeyBindings.TRIGGER_FORCE_MINING_KEY.consumeClick()) { - if (minecraft.player != null) { - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - boolean tabPressed = KeyBindings.SWITCH_CHAIN_MINING_KEY.isDown(); - ModMessages.sendToServer(new TriggerForceMiningPacket(tabPressed)); - } - } - } - } -} - -@Mod.EventBusSubscriber(modid = UselessMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) -class ClientModBusSubscriber { - @SubscribeEvent - public static void onKeyRegister(RegisterKeyMappingsEvent event) { - event.register(KeyBindings.SWITCH_SILK_TOUCH_KEY); - event.register(KeyBindings.SWITCH_FORTUNE_KEY); - event.register(KeyBindings.SWITCH_CHAIN_MINING_KEY); - event.register(KeyBindings.SWITCH_ENHANCED_CHAIN_MINING_KEY); - event.register(KeyBindings.SWITCH_MODE_WHEEL_KEY); - event.register(KeyBindings.SWITCH_FORCE_MINING_KEY); - event.register(KeyBindings.TRIGGER_FORCE_MINING_KEY); - event.register(KeyBindings.SET_MASTER_PATTERN_KEY); - event.register(KeyBindings.SET_SLAVE_PATTERN_KEY); - } -} diff --git a/src/main/java/com/sorrowmist/useless/client/ClientSetup.java b/src/main/java/com/sorrowmist/useless/client/ClientSetup.java deleted file mode 100644 index d941c356..00000000 --- a/src/main/java/com/sorrowmist/useless/client/ClientSetup.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.sorrowmist.useless.blocks.GlowPlasticBlock; -import com.sorrowmist.useless.api.EnumColor; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.client.event.RegisterColorHandlersEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; - -@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT, modid = "useless_mod") -public class ClientSetup { - - @SubscribeEvent - public static void onItemColor(RegisterColorHandlersEvent.Item event) { - // 注册所有颜色的物品染色器 - for (EnumColor color : EnumColor.valuesInOrder()) { - var block = GlowPlasticBlock.GLOW_PLASTIC_BLOCKS.get(color).get(); - event.register((stack, tintIndex) -> { - // 只对 tintIndex 0 应用颜色染色 - return tintIndex == 0 ? color.getRgb() : 0xFFFFFFFF; - }, block.asItem()); - } - } - - @SubscribeEvent - public static void onBlockColor(RegisterColorHandlersEvent.Block event) { - // 注册所有颜色的方块染色器 - for (EnumColor color : EnumColor.valuesInOrder()) { - var block = GlowPlasticBlock.GLOW_PLASTIC_BLOCKS.get(color).get(); - event.register((state, world, pos, tintIndex) -> { - // 只对 tintIndex 0 应用颜色染色 - return tintIndex == 0 ? color.getRgb() : 0xFFFFFFFF; - }, block); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/KeyBindings.java b/src/main/java/com/sorrowmist/useless/client/KeyBindings.java deleted file mode 100644 index f8af32f6..00000000 --- a/src/main/java/com/sorrowmist/useless/client/KeyBindings.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.mojang.blaze3d.platform.InputConstants; -import net.minecraft.client.KeyMapping; -import net.minecraftforge.client.settings.KeyConflictContext; -import org.lwjgl.glfw.GLFW; - -public class KeyBindings { - public static final String KEY_CATEGORY_USELESS = "key.category.useless_mod.useless"; - public static final String KEY_SWITCH_SILK_TOUCH = "key.useless_mod.switch_silk_touch"; - public static final String KEY_SWITCH_FORTUNE = "key.useless_mod.switch_fortune"; - public static final String KEY_SWITCH_CHAIN_MINING = "key.useless_mod.chain_mining"; - public static final String KEY_SWITCH_ENHANCED_CHAIN_MINING = "key.useless_mod.enhanced_chain_mining"; - public static final String KEY_SWITCH_MODE_WHEEL = "key.useless_mod.switch_mode_wheel"; - public static final String KEY_SWITCH_FORCE_MINING = "key.useless_mod.force_mining"; - public static final String KEY_TRIGGER_FORCE_MINING = "key.useless_mod.trigger_force_mining"; - public static final String KEY_SET_MASTER_PATTERN = "key.useless_mod.set_master_pattern"; - public static final String KEY_SET_SLAVE_PATTERN = "key.useless_mod.set_slave_pattern"; - - public static final KeyMapping SWITCH_SILK_TOUCH_KEY = new KeyMapping(KEY_SWITCH_SILK_TOUCH, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_PAGE_DOWN, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_FORTUNE_KEY = new KeyMapping(KEY_SWITCH_FORTUNE, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_PAGE_UP, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_CHAIN_MINING_KEY = new KeyMapping(KEY_SWITCH_CHAIN_MINING, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_TAB, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_ENHANCED_CHAIN_MINING_KEY = new KeyMapping(KEY_SWITCH_ENHANCED_CHAIN_MINING, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_KP_8, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_MODE_WHEEL_KEY = new KeyMapping(KEY_SWITCH_MODE_WHEEL, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_G, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_FORCE_MINING_KEY = new KeyMapping(KEY_SWITCH_FORCE_MINING, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_KP_9, - KEY_CATEGORY_USELESS); - - public static final KeyMapping TRIGGER_FORCE_MINING_KEY = new KeyMapping(KEY_TRIGGER_FORCE_MINING, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_R, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SET_MASTER_PATTERN_KEY = new KeyMapping(KEY_SET_MASTER_PATTERN, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_K, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SET_SLAVE_PATTERN_KEY = new KeyMapping(KEY_SET_SLAVE_PATTERN, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_L, - KEY_CATEGORY_USELESS); - - // 跟踪连锁挖掘按键的按下状态 - public static boolean SWITCH_CHAIN_MINING_KEY_WAS_DOWN = false; -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/ModeWheelHandler.java b/src/main/java/com/sorrowmist/useless/client/ModeWheelHandler.java deleted file mode 100644 index b1027404..00000000 --- a/src/main/java/com/sorrowmist/useless/client/ModeWheelHandler.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.sorrowmist.useless.modes.ModeManager; -import net.minecraft.client.Minecraft; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.client.event.InputEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; - -/** - * 模式轮盘处理器,负责处理模式轮盘的显示、隐藏和交互 - */ -@Mod.EventBusSubscriber(modid = UselessMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) -public class ModeWheelHandler { - /** - * 处理键盘按下事件 - */ - @SubscribeEvent - public static void onKeyPressed(InputEvent.Key event) { - Minecraft minecraft = Minecraft.getInstance(); - if (minecraft.player == null) { - return; - } - - // 检查是否是G键按下事件 (GLFW_PRESS = 1) - if (event.getKey() == KeyBindings.SWITCH_MODE_WHEEL_KEY.getKey().getValue() && event.getAction() == 1) { - // 检查主手和副手物品是否是永恒牛排工具或包含我们NBT数据的Omnitool扳手 - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - ItemStack offHandItem = minecraft.player.getOffhandItem(); - - ItemStack targetItem = null; - - // 检查主手 - net.minecraft.resources.ResourceLocation mainItemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(mainHandItem.getItem()); - if (mainHandItem.getItem() instanceof EndlessBeafItem || - (mainItemId != null && mainItemId.toString().equals("omnitools:omni_wrench") && - mainHandItem.hasTag() && mainHandItem.getTag().contains("ToolModes"))) { - targetItem = mainHandItem; - } - // 检查副手 - else { - net.minecraft.resources.ResourceLocation offItemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(offHandItem.getItem()); - if (offHandItem.getItem() instanceof EndlessBeafItem || - (offItemId != null && offItemId.toString().equals("omnitools:omni_wrench") && - offHandItem.hasTag() && offHandItem.getTag().contains("ToolModes"))) { - targetItem = offHandItem; - } - } - - if (targetItem != null && minecraft.screen == null) { - ModeManager modeManager = new ModeManager(); - modeManager.loadFromStack(targetItem); - ModeWheelScreen.show(modeManager, targetItem); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/ModeWheelScreen.java b/src/main/java/com/sorrowmist/useless/client/ModeWheelScreen.java deleted file mode 100644 index d5a54e89..00000000 --- a/src/main/java/com/sorrowmist/useless/client/ModeWheelScreen.java +++ /dev/null @@ -1,495 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.*; -import com.sorrowmist.useless.modes.ModeManager; -import com.sorrowmist.useless.modes.ToolMode; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; - -import java.util.ArrayList; -import java.util.List; - -/** - * 模式轮盘屏幕,使用Minecraft官方Screen类实现 - */ -public class ModeWheelScreen extends Screen { - // 模式管理器 - private final ModeManager modeManager; - // 主手物品栈 - private final ItemStack mainHandItem; - - // 圆盘菜单参数 - private static final float PRECISION = 5.0f; - private static final int MAX_SLOTS = 18; - private static final float OPEN_ANIMATION_LENGTH = 0.25f; - - private float totalTime; - private float prevTick; - private float extraTick; - private boolean closing = false; - - // 模式数据分组 - private final List单独放一个客户端类,是为了让公共代码({@code EventHandler})不直接引用 + * {@code net.minecraft.client.*}:专用服务器上没有这些类,common 类里出现它们的 + * 符号会在类加载阶段炸掉。这里由调用方保证只在客户端分支里调用。
+ */ +public final class StaffLinkClientHooks { + + private StaffLinkClientHooks() { + } + + /** 按住 Ctrl:批量绑定 / 批量编辑。 */ + public static boolean isBatchModifierDown() { + return Screen.hasControlDown(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/TooltipPageState.java b/src/main/java/com/sorrowmist/useless/client/TooltipPageState.java new file mode 100644 index 00000000..f39d6bc6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/TooltipPageState.java @@ -0,0 +1,91 @@ +package com.sorrowmist.useless.client; + +import net.minecraft.world.item.ItemStack; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 物品提示分页页码的存储。 + * + *页码以物品栈的标识哈希为键。标识哈希在一次游戏会话内唯一且不复用, + * 因此不同物品栈、乃至同一物品的不同堆叠都不会互相干扰。
+ */ +public final class TooltipPageState { + + private static final Map由 {@code EndlessBeafItem#appendHoverText} 在渲染提示框时置位,由客户端渲染帧末复位。 + * 按键事件无法得知提示框是否真的绘制,只能依赖此标记判断翻页键当前是否有效。
+ */ + private static volatile boolean tooltipVisible; + + /** + * 本次渲染提示框所对应的物品栈。 + * + *按键处理与提示框渲染不在同一阶段,按键期间无法回溯到「正在展示哪一份提示」; + * 记录该引用后,按键入口直接对同一物品栈翻页,不必再按主手/副手推断。 + * 持有的是物品栈引用而非副本,与页码缓存使用同一标识哈希,两者键一致。
+ */ + private static volatile ItemStack viewedStack; + + private TooltipPageState() { + } + + /** 记录本次渲染提示框的物品栈。 */ + public static void setViewedStack(ItemStack stack) { + viewedStack = stack; + } + + /** 读取本次渲染提示框的物品栈,无提示框时为 null。 */ + public static ItemStack getViewedStack() { + return viewedStack; + } + + /** 标记提示框本轮已渲染。 */ + public static void markTooltipVisible() { + tooltipVisible = true; + } + + /** 清除提示框可见标记,应在每帧渲染结束后调用。 */ + public static void clearTooltipVisible() { + tooltipVisible = false; + } + + /** 提示框本轮是否已渲染。 */ + public static boolean isTooltipVisible() { + return tooltipVisible; + } + + /** + * 读取页码,不存在时返回 0。 + * + *只读取值而不创建条目:提示渲染每帧都会调用,若此处写入缓存, + * 未翻页的物品栈会长期占用内存。
+ */ + public static int pageOf(int stackIdentity) { + Integer page = PAGES.get(stackIdentity); + return page == null ? 0 : page; + } + + /** + * 翻页并把结果写入存储。 + * + *此处不接收总页数:按键处理发生在提示框渲染之前,调用时无法得知本次提示将产生多少行。 + * 页码只做非负夹紧并单调累加,由 {@code BeefTooltipPager} 在渲染时按总页数取模回绕。
+ * + * @param delta 正值向后翻页,负值向前翻页 + */ + public static void turnPage(int stackIdentity, int delta) { + int current = pageOf(stackIdentity); + PAGES.put(stackIdentity, Math.max(0, current + delta)); + } + + /** 覆写页码,供渲染阶段回绕后写回。 */ + public static void setPage(int stackIdentity, int page) { + PAGES.put(stackIdentity, Math.max(0, page)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/TooltipVisibilityReset.java b/src/main/java/com/sorrowmist/useless/client/TooltipVisibilityReset.java new file mode 100644 index 00000000..f39df85e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/TooltipVisibilityReset.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.client; + +import com.sorrowmist.useless.UselessMod; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.RenderGuiEvent; + +/** + * 提示框可见性标记的帧末复位。 + * + *标记由物品的提示框渲染路径置位,按键事件读取;两者并不在同一阶段,因此需要在每帧 + * 渲染收尾时清除,使标记只反映「刚刚结束的这一帧是否绘制过提示框」。
+ * + *复位放在 {@code RenderGuiEvent.Post}:该项事件的 {@code Post} 阶段处于整帧 GUI 绘制完成之后, + * 早于下一帧的按键分发,不会清除掉同一帧内刚置位的标记。
+ */ +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT) +public final class TooltipVisibilityReset { + + private TooltipVisibilityReset() { + } + + @SubscribeEvent + public static void onRenderGuiPost(RenderGuiEvent.Post event) { + TooltipPageState.clearTooltipVisible(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/AE2StyleTextField.java b/src/main/java/com/sorrowmist/useless/client/gui/AE2StyleTextField.java new file mode 100644 index 00000000..ec79c9cd --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/AE2StyleTextField.java @@ -0,0 +1,109 @@ +package com.sorrowmist.useless.client.gui; + +import net.minecraft.Util; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +/** + * 采用 AE2 文本框外观的输入框。 + * + *AE2 的文本框由 {@code ae2:textures/guis/text_field.png} 一张 128×128 贴图绘制:横向分三段, + * 左端 1 像素、可平铺中段、右端 1 像素;纵向每 12 像素一行,依次对应普通、聚焦与只读三种状态。 + * 该绘制方式仅依赖贴图与 {@code GuiGraphics},不依赖 AE2 的 {@code ScreenStyle} 配置体系, + * 因此可直接复用其外观而不引入 AE2 界面 JSON 依赖。
+ * + *原版 {@link EditBox} 的字体与提示文本字段均为私有且无可用的公开读取方法, + * 因此本类自行保存这两项,绘制时不访问父类私有状态。
+ */ +final class AE2StyleTextField extends EditBox { + /** AE2 文本框贴图,尺寸 128×128。 */ + private static final ResourceLocation TEXTURE = + new ResourceLocation("ae2", "textures/guis/text_field.png"); + /** 贴图中可平铺中段的横向起点,位于左端盖之后。 */ + private static final int SOURCE_BODY_X = 1; + /** 可平铺中段的像素宽度,用作平铺步长。 */ + private static final int SOURCE_BODY_WIDTH = 126; + /** 贴图总宽度,作为 blit 的纹理宽度参数。 */ + private static final int TEXTURE_SIZE = 128; + /** 贴图每行的高度,与三种状态的行距一致。 */ + private static final int ROW_HEIGHT = 12; + /** 文本相对输入框左缘的水平内缩。 */ + private static final int PADDING = 4; + /** 提示文本颜色。 */ + private static final int HINT_COLOR = 0x808080; + /** 正文颜色。 */ + private static final int TEXT_COLOR = 0xE0E0E0; + + /** 绘制用字体,父类字段私有,此处另行保存。 */ + private final Font textFont; + /** 空值且未聚焦时显示的提示文本。 */ + private String hintText = ""; + + AE2StyleTextField(Font font, int x, int y, int width, int height, Component narration) { + super(font, x, y, width, height, narration); + this.textFont = font; + setBordered(false); + } + + /** + * 设置空值提示文本。 + * + *父类的提示由原版在聚焦时绘制,本类改为自绘,故需同时保存一份文本。
+ */ + @Override + public void setHint(Component hint) { + super.setHint(hint); + this.hintText = hint.getString(); + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + if (!isVisible()) return; + + // 贴图纵向三行依次为普通、聚焦、只读:第 0 行为未聚焦底纹,第 1 行为聚焦高亮底纹。 + // 本类不用于只读展示,因此不涉及第 2 行。 + int sourceY = (isFocused() ? 1 : 0) * ROW_HEIGHT; + int x = getX(); + int y = getY(); + int right = x + getWidth(); + // 目标高度取控件实际高度:贴图源行高固定为 ROW_HEIGHT,纵向按需缩放以铺满控件, + // 避免控件高度大于贴图行高时底部露出空隙。 + int height = getHeight(); + + // 左端盖。 + graphics.blit(TEXTURE, x, y, 0, sourceY, 1, height, TEXTURE_SIZE, TEXTURE_SIZE); + // 中段按中段宽度为步长平铺,末段按剩余宽度裁剪,避免贴图被拉伸。 + int bodyX = x + 1; + while (bodyX < right - 1) { + int piece = Math.min(SOURCE_BODY_WIDTH, right - 1 - bodyX); + graphics.blit(TEXTURE, bodyX, y, SOURCE_BODY_X, sourceY, piece, height, + TEXTURE_SIZE, TEXTURE_SIZE); + bodyX += piece; + } + // 右端盖。 + graphics.blit(TEXTURE, right - 1, y, 127, sourceY, 1, height, TEXTURE_SIZE, TEXTURE_SIZE); + + int textY = y + (getHeight() - 8) / 2; + int maxTextWidth = getWidth() - PADDING * 2; + String value = getValue(); + if (value.isEmpty()) { + if (!hintText.isEmpty()) { + graphics.drawString(textFont, textFont.plainSubstrByWidth(hintText, maxTextWidth), + x + PADDING, textY, HINT_COLOR, false); + } + return; + } + + String visible = textFont.plainSubstrByWidth(value, maxTextWidth); + graphics.drawString(textFont, visible, x + PADDING, textY, TEXT_COLOR, false); + + // 光标仅在聚焦时绘制,位置紧随已显示文本。 + if (isFocused() && (Util.getMillis() / 500L) % 2L == 0L) { + int cursorX = x + PADDING + textFont.width(visible); + graphics.fill(cursorX, textY - 1, cursorX + 1, textY + 9, 0xFFD0D0D0); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/AdvancedAlloyFurnaceScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/AdvancedAlloyFurnaceScreen.java new file mode 100644 index 00000000..97789a5b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/AdvancedAlloyFurnaceScreen.java @@ -0,0 +1,1368 @@ +package com.sorrowmist.useless.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import com.sorrowmist.useless.api.enums.RedstoneControlMode; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.content.menus.AdvancedAlloyFurnaceMenu; +import com.sorrowmist.useless.inventory.slot.PatternSlotItemHandler; +import com.sorrowmist.useless.client.render.PatternSlotRenderer; +import com.sorrowmist.useless.network.AutoIOChangePacket; +import com.sorrowmist.useless.network.AECancelPacket; +import com.sorrowmist.useless.network.AEReturnOutputTogglePacket; +import com.sorrowmist.useless.network.FaceModeChangePacket; +import com.sorrowmist.useless.network.PatternPageChangePacket; +import com.sorrowmist.useless.network.RedstoneControlPacket; +import com.sorrowmist.useless.network.TankClearPacket; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import appeng.api.stacks.GenericStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class AdvancedAlloyFurnaceScreen extends AbstractContainerScreen+ * 纹理坐标:U从FurnaceFaceMode.getOverlayU()取,V=265,尺寸11*12。 + */ + private void renderFaceModeOverlays(GuiGraphics guiGraphics, int x, int y) { + if (this.menu.getBlockEntity() == null) return; + FurnaceFaceMode[] modes = this.menu.getBlockEntity().getFaceModes(); + for (int faceIdx = 0; faceIdx < FurnaceFace.COUNT; faceIdx++) { + FurnaceFaceMode mode = modes[faceIdx]; + if (!mode.hasOverlay()) continue; + int u = mode.getOverlayU(); + int faceX = x + FACE_XS[faceIdx]; + int faceY = y + FACE_YS[faceIdx]; + guiGraphics.blit(TEXTURE, + faceX, faceY, + u, OVERLAY_V, + OVERLAY_WIDTH, OVERLAY_HEIGHT, + TEXTURE_WIDTH, TEXTURE_HEIGHT); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) / 2; + + if (this.handlePatternPageClick(mouseX, mouseY, x, y)) return true; + if (this.handleProgressClick(mouseX, mouseY, x, y)) return true; + if (this.handleFluidTankClick(mouseX, mouseY, x, y)) return true; + if (this.handleChemicalTankClick(mouseX, mouseY, x, y)) return true; + if (this.handleFaceModeClick(mouseX, mouseY, button, x, y)) return true; + if (this.handleAutoIOClick(mouseX, mouseY, x, y)) return true; + if (this.handleRedstoneControlClick(mouseX, mouseY, x, y)) return true; + if (this.handleCancelAeClick(mouseX, mouseY, x, y)) return true; + if (this.handleAeReturnClick(mouseX, mouseY, x, y)) return true; + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + this.prevPageButtonPressed = false; + this.nextPageButtonPressed = false; + this.cancelAePressed = false; + return super.mouseReleased(mouseX, mouseY, button); + } + + /** + * 处理翻页按钮点击 + */ + private boolean handlePatternPageClick(double mouseX, double mouseY, int x, int y) { + int currentPage = this.menu.getPatternPage(); + int maxPage = this.menu.getMaxPatternPage(); + + // 左翻页按钮 + if (currentPage > 0 && isInArea(mouseX, mouseY, + x + PREV_PAGE_BUTTON_X, y + PREV_PAGE_BUTTON_Y, + PREV_PAGE_BUTTON_WIDTH, PREV_PAGE_BUTTON_HEIGHT)) { + this.prevPageButtonPressed = true; + int newPage = currentPage - 1; + this.menu.setPatternPage(newPage); + this.updatePatternSlotVisibility(); + PacketDistributor.sendToServer(new PatternPageChangePacket(newPage)); + return true; + } + + // 右翻页按钮 + if (currentPage < maxPage && isInArea(mouseX, mouseY, + x + NEXT_PAGE_BUTTON_X, y + NEXT_PAGE_BUTTON_Y, + NEXT_PAGE_BUTTON_WIDTH, NEXT_PAGE_BUTTON_HEIGHT)) { + this.nextPageButtonPressed = true; + int newPage = currentPage + 1; + this.menu.setPatternPage(newPage); + this.updatePatternSlotVisibility(); + PacketDistributor.sendToServer(new PatternPageChangePacket(newPage)); + return true; + } + + return false; + } + + private boolean handleCancelAeClick(double mouseX, double mouseY, int x, int y) { + if (isInArea(mouseX, mouseY, x + CANCEL_AE_X, y + CANCEL_AE_Y, + CANCEL_AE_WIDTH, CANCEL_AE_HEIGHT)) { + this.cancelAePressed = true; + PacketDistributor.sendToServer(new AECancelPacket( + this.menu.getBlockEntity().getBlockPos())); + return true; + } + return false; + } + + private boolean handleAeReturnClick(double mouseX, double mouseY, int x, int y) { + if (isInArea(mouseX, mouseY, x + AE_RETURN_X, y + AE_RETURN_Y, + AE_RETURN_WIDTH, AE_RETURN_HEIGHT)) { + if (this.menu.getBlockEntity() != null) { + PacketDistributor.sendToServer(new AEReturnOutputTogglePacket( + this.menu.getBlockEntity().getBlockPos())); + } + return true; + } + return false; + } + + /** + * 处理进度条点击(查看配方) + */ + private boolean handleProgressClick(double mouseX, double mouseY, int x, int y) { + if (!isInArea(mouseX, mouseY, + x + PROGRESS_BAR_X, y + PROGRESS_BAR_Y, + PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT)) { + return false; + } + + if (ModList.get().isLoaded("jei")) { + com.sorrowmist.useless.compat.jei.JEIPlugin.showAdvancedAlloyFurnaceRecipes(); + } + return true; + } + + /** + * 处理流体槽点击(清空) + */ + private boolean handleFluidTankClick(double mouseX, double mouseY, int x, int y) { + // 检查输入流体槽 + for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) { + int tankX = x + FLUID_INPUT_X + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + int tankY = y + FLUID_INPUT_Y; + + if (isInArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { + FluidStack fluid = this.menu.getInputFluidTank(i).getFluid(); + if (!fluid.isEmpty()) { + PacketDistributor.sendToServer(new TankClearPacket( + this.menu.getBlockEntity().getBlockPos(), i, true)); + } + return true; + } + } + + // 检查输出流体槽 + for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) { + int tankX = x + FLUID_OUTPUT_X + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + int tankY = y + FLUID_OUTPUT_Y; + + if (isInArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { + FluidStack fluid = this.menu.getOutputFluidTank(i).getFluid(); + if (!fluid.isEmpty()) { + PacketDistributor.sendToServer(new TankClearPacket( + this.menu.getBlockEntity().getBlockPos(), i, false)); + } + return true; + } + } + + return false; + } + + private boolean handleChemicalTankClick(double mouseX, double mouseY, int x, int y) { + if (!this.menu.hasChemicalSupport()) return false; + + for (int i = 0; i < AdvancedAlloyFurnaceLayout.CHEMICAL_TANK_COUNT; i++) { + int tankX = x + CHEMICAL_INPUT_X + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + if (isInArea(mouseX, mouseY, tankX, y + FLUID_INPUT_Y, + FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { + if (!this.menu.getInputChemical(i).isEmpty()) { + PacketDistributor.sendToServer(new TankClearPacket( + this.menu.getBlockEntity().getBlockPos(), i, true, true)); + } + return true; + } + } + + for (int i = 0; i < AdvancedAlloyFurnaceLayout.CHEMICAL_TANK_COUNT; i++) { + int tankX = x + CHEMICAL_OUTPUT_X + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + if (isInArea(mouseX, mouseY, tankX, y + FLUID_OUTPUT_Y, + FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { + if (!this.menu.getOutputChemical(i).isEmpty()) { + PacketDistributor.sendToServer(new TankClearPacket( + this.menu.getBlockEntity().getBlockPos(), i, false, true)); + } + return true; + } + } + return false; + } + + /** + * 处理面模式控制区域点击。 + *
+ * 点击时循环该面的输入输出模式:
+ * 客户端本地立即更新显示,同时发送网络包到服务器。
+ */
+ private boolean handleFaceModeClick(double mouseX, double mouseY, int button, int x, int y) {
+ for (int faceIdx = 0; faceIdx < FurnaceFace.COUNT; faceIdx++) {
+ int faceX = x + FACE_XS[faceIdx];
+ int faceY = y + FACE_YS[faceIdx];
+ if (isInArea(mouseX, mouseY, faceX, faceY, OVERLAY_WIDTH, OVERLAY_HEIGHT)) {
+ FurnaceFace face = FurnaceFace.values()[faceIdx];
+ // 客户端本地立即更新
+ boolean reverse = button == 1;
+ FurnaceFaceMode currentMode = this.menu.getBlockEntity().getFaceMode(face);
+ FurnaceFaceMode newMode = reverse ? currentMode.previous() : currentMode.next();
+ this.menu.getBlockEntity().setFaceMode(face, newMode);
+ // 发送网络包到服务器
+ PacketDistributor.sendToServer(new FaceModeChangePacket(
+ this.menu.getBlockEntity().getBlockPos(), faceIdx, reverse));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 处理自动输入输出配置区域点击。
+ */
+ private boolean handleAutoIOClick(double mouseX, double mouseY, int x, int y) {
+ if (this.menu.getBlockEntity() == null) return false;
+
+ // 自动输出区域
+ if (isInArea(mouseX, mouseY, x + AUTO_OUTPUT_X, y + AUTO_OUTPUT_Y,
+ AUTO_OUTPUT_WIDTH, AUTO_OUTPUT_HEIGHT)) {
+ boolean newState = !this.menu.getBlockEntity().isAutoOutputEnabled();
+ this.menu.getBlockEntity().setAutoOutputEnabled(newState);
+ PacketDistributor.sendToServer(new AutoIOChangePacket(
+ this.menu.getBlockEntity().getBlockPos(), true));
+ return true;
+ }
+
+ // 自动输入区域
+ if (isInArea(mouseX, mouseY, x + AUTO_INPUT_X, y + AUTO_INPUT_Y,
+ AUTO_INPUT_WIDTH, AUTO_INPUT_HEIGHT)) {
+ boolean newState = !this.menu.getBlockEntity().isAutoInputEnabled();
+ this.menu.getBlockEntity().setAutoInputEnabled(newState);
+ PacketDistributor.sendToServer(new AutoIOChangePacket(
+ this.menu.getBlockEntity().getBlockPos(), false));
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 处理红石控制区域点击。
+ */
+ private boolean handleRedstoneControlClick(double mouseX, double mouseY, int x, int y) {
+ if (this.menu.getBlockEntity() == null) return false;
+
+ if (isInArea(mouseX, mouseY, x + REDSTONE_CONTROL_X, y + REDSTONE_CONTROL_Y,
+ REDSTONE_CONTROL_WIDTH, REDSTONE_CONTROL_HEIGHT)) {
+ RedstoneControlMode newMode = this.menu.getBlockEntity().getRedstoneControlMode().next();
+ this.menu.getBlockEntity().setRedstoneControlMode(newMode);
+ PacketDistributor.sendToServer(new RedstoneControlPacket(
+ this.menu.getBlockEntity().getBlockPos()));
+ return true;
+ }
+
+ return false;
+ }
+
+ private void updatePatternSlotVisibility() {
+ int currentPage = this.menu.getPatternPage();
+ int slotsPerPage = this.menu.getPatternSlotsPerPage();
+ int base = currentPage * slotsPerPage;
+ int end = Math.min(base + slotsPerPage, AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_COUNT);
+
+ for (Slot slot : this.menu.slots) {
+ if (slot instanceof com.sorrowmist.useless.inventory.slot.PatternSlotItemHandler patternSlot) {
+ int slotIndex = slot.getSlotIndex();
+ int relativeIndex = slotIndex - AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_START;
+ boolean isActive = relativeIndex >= base && relativeIndex < end;
+ patternSlot.setActive(isActive);
+ }
+ }
+ }
+
+ private static boolean isInArea(double mouseX, double mouseY, int areaX, int areaY, int width, int height) {
+ return mouseX >= areaX && mouseX < areaX + width && mouseY >= areaY && mouseY < areaY + height;
+ }
+
+ private void renderFluidTankTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y,
+ boolean isInput) {
+ int startX = isInput ? FLUID_INPUT_X : FLUID_OUTPUT_X;
+ int startY = isInput ? FLUID_INPUT_Y : FLUID_OUTPUT_Y;
+
+ for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) {
+ int tankX = x + startX + i * (FLUID_TANK_WIDTH + SLOT_SPACING);
+ int tankY = y + startY;
+
+ if (!isInArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) continue;
+
+ FluidStack fluid = isInput ?
+ this.menu.getInputFluidTank(i).getFluid() :
+ this.menu.getOutputFluidTank(i).getFluid();
+ int capacity = isInput ?
+ this.menu.getInputFluidTank(i).getCapacity() :
+ this.menu.getOutputFluidTank(i).getCapacity();
+
+ if (!fluid.isEmpty()) {
+ Component fluidName = fluid.getDisplayName();
+ Component amountText = Component.translatable("gui.useless_mod.advanced_alloy_furnace.fluid_amount",
+ String.format("%,d", fluid.getAmount()),
+ String.format("%,d", capacity));
+
+ guiGraphics.renderTooltip(this.font, List.of(fluidName, amountText),
+ Optional.empty(), mouseX, mouseY);
+ }
+ break;
+ }
+ }
+
+ private void renderChemicalTankTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY,
+ int x, int y, boolean isInput) {
+ if (!this.menu.hasChemicalSupport()) return;
+ int startX = isInput ? CHEMICAL_INPUT_X : CHEMICAL_OUTPUT_X;
+ int startY = isInput ? FLUID_INPUT_Y : FLUID_OUTPUT_Y;
+ for (int i = 0; i < AdvancedAlloyFurnaceLayout.CHEMICAL_TANK_COUNT; i++) {
+ int tankX = x + startX + i * (FLUID_TANK_WIDTH + SLOT_SPACING);
+ int tankY = y + startY;
+ if (!isInArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) continue;
+
+ ChemicalStackView chemical = isInput
+ ? this.menu.getInputChemical(i) : this.menu.getOutputChemical(i);
+ if (!chemical.isEmpty()) {
+ Component amountText = Component.translatable(
+ "gui.useless_mod.advanced_alloy_furnace.chemical_amount",
+ formatLong(chemical.amount()), formatLong(this.menu.getChemicalTankCapacity()));
+ guiGraphics.renderTooltip(this.font,
+ List.of(chemical.displayName(), amountText), Optional.empty(), mouseX, mouseY);
+ }
+ break;
+ }
+ }
+
+ private static String formatLong(long amount) {
+ return String.format("%,d", amount);
+ }
+
+ private void renderEnergyTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) {
+ if (!isInArea(mouseX, mouseY,
+ x + ENERGY_BAR_X, y + ENERGY_BAR_Y,
+ ENERGY_BAR_WIDTH, ENERGY_BAR_HEIGHT)) {
+ return;
+ }
+
+ List 从模式轮盘(G)→「连锁挖掘」模块右端的齿轮进入。等价组是玩家个人设置,
+ * 与造化杖布局存在同一份 {@link BeefToolLayout} 里,所以编辑直接走
+ * {@link BeefToolLayoutUpdatePacket}——与轮盘的导入导出是同一条校验/落库路径。 本界面是 {@link AbstractContainerScreen} 而非普通 {@code Screen}:JEI 与 EMI 的原料侧栏
+ * 默认只画在容器界面旁边(JEI 内置的 handler 只覆盖 {@code AbstractContainerScreen}),
+ * 普通 {@code Screen} 拿不到侧栏也就没法拖拽。菜单是空的、也不下发,
+ * 数据仍然走服务端权威的布局更新包。 面板默认居中;只有当居中会把右侧挤到不足这个宽度时,才自动往左挪。
+ * 取 120 是让侧栏至少能出 6 列(12px 边距 + 6×18px),而不是勉强压着 48px 的下限。 输入框里还有没提交的内容时整份跳过:这些内容只会在玩家按回车或点「添加」时
+ * 才变成条目,被快照覆盖掉就等于把玩家打的字吞了。 父类 {@code renderBackground} 先铺半透明底、再调本方法;之后才是
+ * JEI 的侧栏背景、控件(renderables)、槽位与标签,最后是 JEI 的物品格子。 列表空态提示长于列宽,直接截断会丢失后半句语义,因此折行完整呈现。
+ * 行距取字体行高加 1 像素,避免相邻两行贴连;不使用列表行高,否则行距过疏。 取值沿用 AE2 对未启用可选槽位的淡化系数:未放入容量卡而不可用的区域仍保留底板轮廓,
+ * 但与面板按该透明度混合后明显浅于可用槽位。混合交由渲染管线完成,面板色调整时
+ * 无需重新推导常量。 淡化由渲染管线的颜色混合完成,而非改用更浅的字面量颜色,使高亮边框与底部暗边在
+ * 同一系数下一起变淡;颜色字面量各自减淡会让两层失去原有的相对明暗关系。
+ * 主手与副手依次查找,任一持有即返回对应物品;双手均未持有时返回 {@code null}。
+ * 判定范围与连锁高亮一致,覆盖基础形态与全部工具模式变体。
+ */
+ @Nullable
+ private static ItemStack resolveHeldBeafTool(LocalPlayer player) {
+ for (InteractionHand hand : InteractionHand.values()) {
+ ItemStack stack = player.getItemInHand(hand);
+ if (BeefToolVariants.isBeafTool(stack)) {
+ return stack;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/sorrowmist/useless/client/gui/ModeWheelScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/ModeWheelScreen.java
new file mode 100644
index 00000000..44b17f5f
--- /dev/null
+++ b/src/main/java/com/sorrowmist/useless/client/gui/ModeWheelScreen.java
@@ -0,0 +1,1745 @@
+package com.sorrowmist.useless.client.gui;
+
+import com.mojang.blaze3d.platform.InputConstants;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.sorrowmist.useless.api.enums.tool.ConstructionWandCoreMode;
+import com.sorrowmist.useless.api.enums.tool.EnchantMode;
+import com.sorrowmist.useless.api.enums.tool.ToolTypeMode;
+import com.sorrowmist.useless.client.BeefAutoClicker;
+import com.sorrowmist.useless.content.menus.ChainGroupMenu;
+import com.sorrowmist.useless.core.common.KeyBindings;
+import com.sorrowmist.useless.core.component.UComponents;
+import com.sorrowmist.useless.core.config.BeefToolProtectionManager;
+import com.sorrowmist.useless.core.config.ChainGroupManager;
+import com.sorrowmist.useless.content.items.EndlessBeafItem;
+import com.sorrowmist.useless.data.BeefToolLayout;
+import com.sorrowmist.useless.data.BeefToolModuleRegistry;
+import com.sorrowmist.useless.network.BeefToolLayoutRequestPacket;
+import com.sorrowmist.useless.network.BeefToolLayoutUpdatePacket;
+import com.sorrowmist.useless.network.ConstructionWandCorePacket;
+import com.sorrowmist.useless.network.EnchantmentSwitchPacket;
+import com.sorrowmist.useless.network.ModeTogglePacket;
+import com.sorrowmist.useless.network.ToolTypeModeSwitchPacket;
+import com.sorrowmist.useless.utils.UselessItemUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.EditBox;
+import net.minecraft.client.gui.components.Renderable;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.player.Inventory;
+import net.minecraft.world.item.ItemStack;
+import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor;
+import org.lwjgl.glfw.GLFW;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ModeWheelScreen extends Screen {
+ private static final int PANEL_MAX_WIDTH = 480;
+ private static final int PANEL_MARGIN = 6;
+ private static final int HEADER_HEIGHT = 42;
+ private static final int NORMAL_FOOTER_HEIGHT = 20;
+ private static final int EDITOR_FOOTER_HEIGHT = 38;
+ private static final int CONTENT_PADDING = 4;
+ private static final int CARD_GAP = 5;
+ private static final int CARD_HEADER_HEIGHT = 18;
+ private static final int CARD_BOTTOM_PADDING = 6;
+ private static final int MODULE_HEIGHT = 18;
+ private static final int MODULE_GAP = 2;
+ /** 连锁等价组齿轮图标的边长(画在连锁挖掘模块按钮右端)。 */
+ private static final int GEAR_SIZE = 12;
+ private static final int PAGE_TAB_HEIGHT = 15;
+ private static final int PAGE_TAB_WIDTH = 82;
+ private static final int DRAG_THRESHOLD = 4;
+ private static final int DESIGN_HEIGHT = 320;
+
+ private final List 用 {@code fill} 拼出轮廓,不新增贴图。 只有需要额外说明「怎么操作」的模块才登记,其余一律不显示,免得把轮盘糊满。 必须「一行一个 Component」:{@code GuiGraphics#renderTooltip(Font, Component, int, int)}
+ * 走的是 {@code Component#getVisualOrderText()},它把整串文字当成一行做 bidi 重排,
+ * 翻译文本里的 {@code \n} 不会变成换行(只会渲染成一个缺字形)。
+ * 只有传多行列表({@code renderComponentTooltip})才会逐行渲染。 搜索按模具物品名与配方登记的可接受物品名匹配,使按任意可接受物品名都能定位到对应模具。
+ * 内容全部为模具,不存在输入与输出的区分,因此不提供搜索范围选择;搜索范围由基类限定在
+ * 活跃模具槽位,只读恢复页内容不进入结果。> groups = layout.chainGroups();
+ if (groups.isEmpty()) {
+ // 与正常条目行同基线(LIST_TOP + 4),否则会比列表内衬顶边高出 4px 而压在那条边框线上。
+ // 提示按列宽折行:该文案长于列宽,截断会丢失后半句语义。
+ int budget = GROUP_COLUMN_RIGHT - (CONTENT_LEFT + 2) - 2;
+ drawWrapped(graphics, Component.translatable("gui.useless_mod.chain_group.empty"),
+ leftPos + CONTENT_LEFT + 2, topPos + LIST_TOP + 4, budget);
+ return;
+ }
+
+ for (int row = 0; row < LIST_VISIBLE_ROWS; row++) {
+ int index = groupScroll + row;
+ if (index >= groups.size()) {
+ break;
+ }
+ List