Merge pull request #11 from C-H716/feature/autoPattern

Feature/autoPattern
This commit is contained in:
GaLicn 2025-08-29 15:10:44 +08:00 committed by GitHub
commit 38eb1788cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 563 additions and 3 deletions

View File

@ -102,6 +102,10 @@ dependencies {
//jec
modCompileOnly "curse.maven:just-enough-characters-250702:6680042"
//mae2
// modRuntimeOnly "curse.maven:modern-ae2-additions-1028068:6342203"
modCompileOnly "curse.maven:modern-ae2-additions-1028068:6342203"
}
allprojects {

View File

@ -0,0 +1,136 @@
package com.extendedae_plus.content;
import appeng.api.crafting.IPatternDetails;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
import appeng.api.stacks.KeyCounter;
import appeng.crafting.pattern.AEProcessingPattern;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/**
* 缩放后的处理样板结构完全模拟 AEProcessingPattern
* 保持 sparse/condensed/inputs 的一致性同时保存原始样板
*/
public final class ScaledProcessingPattern implements IPatternDetails {
private final AEProcessingPattern original; // 原始样板引用
private final AEItemKey definition; // 样板物品
private final GenericStack[] sparseInputs; // 缩放后的稀疏输入
private final GenericStack[] sparseOutputs; // 缩放后的稀疏输出
private final IInput[] inputs; // 缩放后的压缩输入
private final GenericStack[] condensedOutputs; // 缩放后的压缩输出
public ScaledProcessingPattern(
AEProcessingPattern original,
AEItemKey definition,
GenericStack[] sparseInputs,
GenericStack[] sparseOutputs,
IInput[] inputs,
GenericStack[] condensedOutputs
) {
this.original = Objects.requireNonNull(original);
this.definition = Objects.requireNonNull(definition);
this.sparseInputs = Objects.requireNonNull(sparseInputs);
this.sparseOutputs = Objects.requireNonNull(sparseOutputs);
this.inputs = Objects.requireNonNull(inputs);
this.condensedOutputs = Objects.requireNonNull(condensedOutputs);
}
/* -------------------- API 实现 -------------------- */
public AEProcessingPattern getOriginal() {
return original;
}
@Override
public AEItemKey getDefinition() {
return definition;
}
@Override
public IInput[] getInputs() {
return inputs;
}
@Override
public GenericStack[] getOutputs() {
return condensedOutputs;
}
public GenericStack[] getSparseInputs() {
return sparseInputs;
}
public GenericStack[] getSparseOutputs() {
return sparseOutputs;
}
@Override
public GenericStack getPrimaryOutput() {
if (condensedOutputs.length > 0) return condensedOutputs[0];
return original.getPrimaryOutput();
}
@Override
public boolean supportsPushInputsToExternalInventory() {
return original.supportsPushInputsToExternalInventory();
}
@Override
public void pushInputsToExternalInventory(KeyCounter[] inputHolder, PatternInputSink inputSink) {
// 保持和 AEProcessingPattern 一致 sparseInputs 驱动
if (sparseInputs.length == inputs.length) {
IPatternDetails.super.pushInputsToExternalInventory(inputHolder, inputSink);
} else {
KeyCounter allInputs = new KeyCounter();
for (KeyCounter counter : inputHolder) {
allInputs.addAll(counter);
}
for (GenericStack sparseInput : sparseInputs) {
if (sparseInput != null) {
AEKey key = sparseInput.what();
long amount = sparseInput.amount();
long available = allInputs.get(key);
if (available < amount) {
throw new RuntimeException("Expected at least %d of %s when pushing scaled pattern, but only %d available"
.formatted(amount, key, available));
}
inputSink.pushInput(key, amount);
allInputs.remove(key, amount);
}
}
}
}
/* -------------------- 缩放输入代理 -------------------- */
public static final class Input implements IPatternDetails.IInput {
private final GenericStack[] template;
private final long multiplier;
public Input(GenericStack[] template, long multiplier) {
this.template = template;
this.multiplier = multiplier;
}
public GenericStack[] getPossibleInputs() {
return this.template;
}
public long getMultiplier() {
return this.multiplier;
}
public boolean isValid(AEKey input, Level level) {
return input.matches(this.template[0]);
}
public @Nullable AEKey getRemainingKey(AEKey template) {
return null;
}
}
}

View File

@ -0,0 +1,15 @@
package com.extendedae_plus.mixin.autopattern;
import appeng.api.stacks.AEKey;
import appeng.crafting.CraftingCalculation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(CraftingCalculation.class)
public interface CraftingCalculationAccessor {
@Accessor("output")
AEKey extendedae_plus$getOutput();
@Accessor("requestedAmount")
long extendedae_plus$getRequestedAmount();
}

View File

@ -0,0 +1,28 @@
package com.extendedae_plus.mixin.autopattern;
import appeng.api.crafting.IPatternDetails;
import appeng.me.service.CraftingService;
import com.extendedae_plus.content.ScaledProcessingPattern;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
/**
* CraftingService.getProviders 调用点修改传入的 IPatternDetails 参数回退到网络注册的原始样板
*/
@Mixin(value = CraftingService.class, remap = false)
public class CraftingServiceGetProvidersMixin {
@ModifyArg(method = "getProviders(Lappeng/api/crafting/IPatternDetails;)Ljava/lang/Iterable;",
at = @At(value = "INVOKE", target = "Lappeng/me/service/helpers/NetworkCraftingProviders;getMediums(Lappeng/api/crafting/IPatternDetails;)Ljava/lang/Iterable;"),
index = 0)
private IPatternDetails extendedae_plus$modifyGetProvidersArg(IPatternDetails original) {
IPatternDetails base = null;
if (original instanceof ScaledProcessingPattern scaledProcessingPattern) {
base = scaledProcessingPattern.getOriginal();
}
return base == null ? original : base;
}
}

View File

@ -0,0 +1,16 @@
package com.extendedae_plus.mixin.autopattern;
import appeng.api.stacks.AEKey;
import appeng.crafting.CraftingTreeNode;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(CraftingTreeNode.class)
public interface CraftingTreeNodeAccessor {
@Accessor("what")
AEKey extendedae_plus$getWhat();
@Accessor("amount")
long extendedae_plus$getAmount();
}

View File

@ -0,0 +1,30 @@
package com.extendedae_plus.mixin.autopattern;
import appeng.api.stacks.KeyCounter;
import appeng.crafting.CraftingTreeNode;
import appeng.crafting.inv.CraftingSimulationState;
import com.extendedae_plus.util.RequestedAmountHolder;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
@Mixin(value = CraftingTreeNode.class,remap = false)
public class CraftingTreeNodeMixin {
@Inject(method = "request(Lappeng/crafting/inv/CraftingSimulationState;JLappeng/api/stacks/KeyCounter;)V",
at = @At(value = "INVOKE",
target = "Lappeng/crafting/CraftingTreeNode;addContainerItems(Lappeng/api/stacks/AEKey;JLappeng/api/stacks/KeyCounter;)V"),
locals = LocalCapture.CAPTURE_FAILHARD)
private void captureRequestedAmount(CraftingSimulationState inv, long requestedAmount, KeyCounter containerItems, CallbackInfo ci) {
// push the requestedAmount before addContainerItems is called
RequestedAmountHolder.push(requestedAmount);
}
@Inject(method = "request(Lappeng/crafting/inv/CraftingSimulationState;JLappeng/api/stacks/KeyCounter;)V",
at = @At(value = "RETURN"))
private void clearRequestedAmountOnReturn(CraftingSimulationState inv, long requestedAmount, KeyCounter containerItems, CallbackInfo ci) {
// pop the pushed requested amount on return
RequestedAmountHolder.pop();
}
}

View File

@ -0,0 +1,50 @@
package com.extendedae_plus.mixin.autopattern;
import appeng.api.crafting.IPatternDetails;
import appeng.api.networking.crafting.ICraftingService;
import appeng.api.stacks.AEKey;
import appeng.crafting.CraftBranchFailure;
import appeng.crafting.CraftingCalculation;
import appeng.crafting.CraftingTreeNode;
import appeng.crafting.CraftingTreeProcess;
import appeng.crafting.inv.CraftingSimulationState;
import appeng.crafting.pattern.AEProcessingPattern;
import com.extendedae_plus.util.PatternScaler;
import com.extendedae_plus.util.RequestedAmountHolder;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
/**
* 注入 CraftingTreeProcess 构造器尾部 AEProcessingPattern 替换为 ScaledProcessingPattern
* 以确保后续执行使用放大后的输入/输出视图
*/
@Mixin(CraftingTreeProcess.class)
public abstract class CraftingTreeProcessMixin {
@Shadow abstract void request(CraftingSimulationState inv, long times) throws CraftBranchFailure, InterruptedException;
@ModifyVariable(
method = "<init>(Lappeng/api/networking/crafting/ICraftingService;Lappeng/crafting/CraftingCalculation;Lappeng/api/crafting/IPatternDetails;Lappeng/crafting/CraftingTreeNode;)V",
at = @At("HEAD"),
argsOnly = true
)
private static IPatternDetails extendedae_plus$replaceDetailsAtHead(IPatternDetails original, ICraftingService cc, CraftingCalculation job, IPatternDetails details, CraftingTreeNode craftingTreeNode) {
try {
if (!(details instanceof AEProcessingPattern proc)) return original;
CraftingTreeNodeAccessor parentAcc = (CraftingTreeNodeAccessor) craftingTreeNode;
AEKey parentTarget = parentAcc.extendedae_plus$getWhat();
long requested = RequestedAmountHolder.get();
// 使用当前线程栈顶的值进行缩放不在此处清理构造完成后应该由调用方的 pop 恢复状态
return PatternScaler.scale(proc, parentTarget, requested);
} catch (Exception e) {
LOGGER.warn("构建倍增样板出错", e);
e.printStackTrace();
return original;
}
}
}

View File

@ -0,0 +1,39 @@
package com.extendedae_plus.mixin.autopattern;
import appeng.api.crafting.IPatternDetails;
import appeng.helpers.patternprovider.PatternProviderLogic;
import com.extendedae_plus.content.ScaledProcessingPattern;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.List;
/**
* Redirect PatternProviderLogic.pushPattern 中对 List.contains 的调用
* 在遇到缩放样板时回退匹配到原始样板实例
*/
@Mixin(value = PatternProviderLogic.class, remap = false)
public class PatternProviderLogicContainsRedirectMixin {
@Redirect(method = "pushPattern",
at = @At(
value = "INVOKE",
target = "Ljava/util/List;contains(Ljava/lang/Object;)Z")
)
private boolean extendedae_plus$patternsContains(List<?> list, Object o) {
try {
if (o instanceof ScaledProcessingPattern scaled) {
IPatternDetails base = scaled.getOriginal();
if (base != null && list.indexOf(base) != -1) {
return true;
}
}
// 使用 indexOf 避免再次触发对 List.contains redirect防止递归
return list.indexOf(o) != -1;
} catch (Throwable t) {
t.printStackTrace();
return list.indexOf(o) != -1;
}
}
}

View File

@ -0,0 +1,78 @@
package com.extendedae_plus.util;
import java.util.Arrays;
public class ArraySimplifier {
// 计算两个数的GCD using Euclidean algorithm (long版本)
public static long gcd(long a, long b) {
while (b != 0) {
long temp = b;
b = a % b;
a = temp;
}
return a;
}
// 计算整个数组的GCD
public static long findGCD(long[] arr) {
if (arr.length == 0) {
return 0;
}
long result = arr[0];
for (int i = 1; i < arr.length; i++) {
result = gcd(result, arr[i]);
// 如果已经找到GCD为1可以提前终止
if (result == 1) {
break;
}
}
return result;
}
// 简化数组每个元素除以数组的GCD
public static long[] simplifyFraction(long[] arr) {
if (arr.length == 0) {
return new long[0];
}
long gcd = findGCD(arr);
if (gcd == 0) {
// 如果GCD为0所有元素为0返回原数组的副本
return Arrays.copyOf(arr, arr.length);
}
long[] simplified = new long[arr.length];
for (int i = 0; i < arr.length; i++) {
simplified[i] = arr[i] / gcd;
}
return simplified;
}
// 将两个数组合并为一个新数组先放 a 后放 b
public static long[] combine(long[] a, long[] b) {
long[] out = new long[a.length + b.length];
System.arraycopy(a, 0, out, 0, a.length);
System.arraycopy(b, 0, out, a.length, b.length);
return out;
}
// 寻找数组的 GCD遇到 1 则立即返回 1早期退出优化
public static long findGCDWithEarlyExit(long[] arr) {
if (arr.length == 0) return 0;
long result = 0;
for (long v : arr) {
if (v == 1) return 1; // already irreducible
if (v == 0) continue;
if (result == 0) result = v; else result = gcd(result, v);
if (result == 1) return 1;
}
return result == 0 ? 0 : Math.abs(result);
}
// 根据给定的 gcd 返回一个已除以 gcd 的新数组如果 gcd==1 返回原数组避免不必要的分配
public static long[] simplifyByGcd(long[] arr, long gcd) {
if (gcd <= 1) return arr;
long[] out = new long[arr.length];
for (int i = 0; i < arr.length; i++) out[i] = arr[i] / gcd;
return out;
}
}

View File

@ -0,0 +1,114 @@
package com.extendedae_plus.util;
import appeng.api.crafting.IPatternDetails.IInput;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
import appeng.crafting.pattern.AEProcessingPattern;
import com.extendedae_plus.content.ScaledProcessingPattern;
import java.util.Arrays;
import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
public final class PatternScaler {
private PatternScaler() {
}
public static ScaledProcessingPattern scale(AEProcessingPattern base, AEKey target, long requestedAmount) {
if (base == null) throw new IllegalArgumentException("base");
if (target == null) throw new IllegalArgumentException("target");
GenericStack[] baseSparseInputs = base.getSparseInputs();
GenericStack[] baseSparseOutputs = base.getSparseOutputs();
IInput[] baseInputs = base.getInputs();
GenericStack[] baseOutputs = base.getOutputs();
// 新逻辑不再对样板进行单位化处理
// 找到目标输出在 outputs 中的索引尝试匹配 target否则取第一个非空输出
int targetOutIndex = -1;
for (int i = 0; i < baseOutputs.length; i++) {
var out = baseOutputs[i];
if (out != null && target != null && out.what() != null && out.what().equals(target)) {
targetOutIndex = i;
break;
}
}
if (targetOutIndex == -1) {
for (int i = 0; i < baseOutputs.length; i++) {
if (baseOutputs[i] != null) {
targetOutIndex = i;
break;
}
}
}
if (targetOutIndex == -1 && baseOutputs.length > 0) targetOutIndex = 0;
long perOperationTarget = 1L;
if (targetOutIndex >= 0 && baseOutputs[targetOutIndex] != null) {
long amt = baseOutputs[targetOutIndex].amount();
if (amt > 0) perOperationTarget = amt;
}
// 使用最小整数倍ceil策略直接选择满足请求的最小倍数
long multiplier = 1L;
if (requestedAmount > 0) {
long needed = requestedAmount / perOperationTarget + ((requestedAmount % perOperationTarget) == 0 ? 0 : 1);
multiplier = needed <= 1L ? 1L : needed;
}
// 构建压缩输入将每个输入的 multiplier 翻倍保留每个模板的原始数量
IInput[] scaledInputs = new IInput[baseInputs.length];
for (int i = 0; i < baseInputs.length; i++) {
var in = baseInputs[i];
var template = in.getPossibleInputs();
GenericStack[] scaledTemplates = new GenericStack[template.length];
for (int j = 0; j < template.length; j++) {
scaledTemplates[j] = new GenericStack(template[j].what(), template[j].amount());
}
scaledInputs[i] = new ScaledProcessingPattern.Input(scaledTemplates, in.getMultiplier() * multiplier);
}
/* 4. 构建压缩输出 */
GenericStack[] scaledCondensedOutputs = new GenericStack[baseOutputs.length];
for (int i = 0; i < baseOutputs.length; i++) {
GenericStack out = baseOutputs[i];
if (out != null) {
scaledCondensedOutputs[i] = new GenericStack(out.what(), out.amount() * multiplier);
}
}
// 构建并打印稀疏表示直接按 multiplier 放大
GenericStack[] scaledSparseInputs = new GenericStack[baseSparseInputs.length];
for (int i = 0; i < baseSparseInputs.length; i++) {
var in = baseSparseInputs[i];
if (in != null) {
scaledSparseInputs[i] = new GenericStack(in.what(), in.amount() * multiplier);
}
}
GenericStack[] scaledSparseOutputs = new GenericStack[baseSparseOutputs.length];
for (int i = 0; i < baseSparseOutputs.length; i++) {
var out = baseSparseOutputs[i];
if (out != null) {
scaledSparseOutputs[i] = new GenericStack(out.what(), out.amount() * multiplier);
}
}
/* Debug 输出 */
LOGGER.info("[extendedae_plus] 正在缩放样板: 目标物品: {} 请求数量: {} 缩放后输入: {} 缩放后输出: {} 缩放后稀疏输入: {} 缩放后稀疏输出: {}",
target,
requestedAmount,
Arrays.toString(scaledInputs),
Arrays.toString(scaledCondensedOutputs),
Arrays.toString(scaledSparseInputs),
Arrays.toString(scaledSparseOutputs));
return new ScaledProcessingPattern(base,
base.getDefinition(),
scaledSparseInputs,
scaledSparseOutputs,
scaledInputs,
scaledCondensedOutputs);
}
}

View File

@ -0,0 +1,44 @@
package com.extendedae_plus.util;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* Thread-local stack holder for requested amounts to support nested requests.
*/
public final class RequestedAmountHolder {
private static final ThreadLocal<Deque<Long>> HOLDER = ThreadLocal.withInitial(ArrayDeque::new);
private RequestedAmountHolder() {
}
/**
* Push a requested amount onto the thread-local stack.
*/
public static void push(long v) {
Deque<Long> dq = HOLDER.get();
dq.push(v);
}
/**
* Pop the top value from the thread-local stack. Safe if empty.
*/
public static void pop() {
Deque<Long> dq = HOLDER.get();
if (dq.isEmpty()) {
return;
}
dq.pop();
}
/**
* Peek the current requested amount or return 0 if none.
*/
public static long get() {
Deque<Long> dq = HOLDER.get();
Long v = dq.peek();
return v == null ? 0L : v;
}
}

View File

@ -22,11 +22,12 @@
"extendedae.HighlightButtonMixin",
"extendedae.accessor.GuiExPatternTerminalAccessor",
"extendedae.accessor.GuiExPatternTerminalSlotsRowAccessor",
"jei.EncodePatternTransferHandlerMixin",
"hooks.ModelBakeryMixin"
"hooks.ModelBakeryMixin",
"jei.EncodePatternTransferHandlerMixin"
],
"mixins": [
"ae2.ContainerPatternEncodingTermMenuMixin",
"ae2.CraftingCPUClusterMixin",
"ae2.MEStorageMenuMixin",
"ae2.PatternEncodingTermMenuMixin",
"ae2.PatternProviderLogicAdvancedMixin",
@ -37,10 +38,15 @@
"ae2.accessor.PatternProviderLogicPatternInputsAccessor",
"ae2.accessor.PatternProviderMenuAdvancedAccessor",
"ae2WTlib.ContainerUWirelessExPatternTerminalMixin",
"autopattern.CraftingCalculationAccessor",
"autopattern.CraftingServiceGetProvidersMixin",
"autopattern.CraftingTreeNodeAccessor",
"autopattern.CraftingTreeNodeMixin",
"autopattern.CraftingTreeProcessMixin",
"autopattern.PatternProviderLogicContainsRedirectMixin",
"extendedae.ContainerExPatternProviderMixin",
"extendedae.ContainerExPatternTerminalMixin",
"extendedae.ContainerWirelessExPatternTerminalMixin",
"ae2.CraftingCPUClusterMixin",
"extendedae.PartExPatternProviderMixin",
"extendedae.TileExPatternProviderMixin"
],