diff --git a/src/main/java/com/gtocore/common/machine/multiblock/part/ae/MEPatternContentSortMachine.kt b/src/main/java/com/gtocore/common/machine/multiblock/part/ae/MEPatternContentSortMachine.kt index 77c2d65d2..72f556d6f 100644 --- a/src/main/java/com/gtocore/common/machine/multiblock/part/ae/MEPatternContentSortMachine.kt +++ b/src/main/java/com/gtocore/common/machine/multiblock/part/ae/MEPatternContentSortMachine.kt @@ -5,6 +5,8 @@ import com.gtocore.api.gui.ktflexible.progressBar import com.gtocore.api.gui.ktflexible.textBlock import com.gtocore.common.machine.multiblock.part.ae.MEPatternContentSortMachine.MODE.FLUID import com.gtocore.common.machine.multiblock.part.ae.MEPatternContentSortMachine.MODE.ITEM +import com.gtocore.utils.AEKeySubstitutionMap +import com.gtocore.utils.AEPatternRefresher import net.minecraft.network.chat.Component import net.minecraft.server.TickTask @@ -16,8 +18,6 @@ import appeng.api.networking.IManagedGridNode import appeng.api.stacks.AEFluidKey import appeng.api.stacks.AEItemKey import appeng.api.stacks.AEKey -import appeng.blockentity.crafting.PatternProviderBlockEntity -import appeng.me.Grid import com.gregtechceu.gtceu.api.blockentity.MetaMachineBlockEntity import com.gregtechceu.gtceu.api.gui.fancy.FancyMachineUIWidget import com.gregtechceu.gtceu.api.gui.fancy.IFancyUIProvider @@ -26,7 +26,6 @@ import com.gregtechceu.gtceu.api.machine.TickableSubscription import com.gregtechceu.gtceu.api.machine.feature.IFancyUIMachine import com.gregtechceu.gtceu.integration.ae2.machine.feature.IGridConnectedMachine import com.gregtechceu.gtceu.integration.ae2.machine.trait.GridNodeHolder -import com.gtolib.api.ae2.IExpandedGrid import com.gtolib.api.annotation.DataGeneratorScanned import com.gtolib.api.annotation.language.RegisterLanguage import com.gtolib.api.gui.ktflexible.button @@ -39,15 +38,10 @@ import com.lowdragmc.lowdraglib.misc.ItemTransferList import com.lowdragmc.lowdraglib.syncdata.IContentChangeAware import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted -import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap -import it.unimi.dsi.fastutil.objects.Object2ObjectMap -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArrayList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import java.util.concurrent.ConcurrentHashMap - @DataGeneratorScanned class MEPatternContentSortMachine(holder: MetaMachineBlockEntity) : MetaMachine(holder), @@ -95,146 +89,95 @@ class MEPatternContentSortMachine(holder: MetaMachineBlockEntity) : class FlowData(val finished: Int, val total: Int) abstract fun applyRefresh() abstract fun receiveFlowData(flowData: FlowData) - abstract fun collectFlowRunner(): List - abstract fun buildCoroutine(run: List): CoroutineScope } private inner class InternalLogic : InternalLogicTemplate() { var lastFlowData: FlowData? = null var uniqueScope: CoroutineScope? = null var isRefreshing: Boolean = false override fun applyRefresh() { - val runnables = collectFlowRunner() uniqueScope?.cancel() - uniqueScope = buildCoroutine(runnables) - } + val grid = gridNodeHolder.mainNode?.grid ?: return + uniqueScope = CoroutineScope(Dispatchers.Default) - override fun receiveFlowData(flowData: FlowData) { - lastFlowData = flowData - } - - override fun collectFlowRunner(): List { - val runnable = mutableListOf() - if (gridNodeHolder.mainNode?.isActive == false) return runnable - val grid = gridNodeHolder.mainNode?.grid ?: return runnable - grid.getActiveMachines(PatternProviderBlockEntity::class.java).forEach { - runnable.add { it.logic.updatePatterns() } - } - if (grid is Grid && grid is IExpandedGrid) { - val machinesPart = mutableListOf>() - grid.machines.forEach { _, node -> - if (node.isActive) { - val logicHost = node.owner - if (logicHost is MEPatternPartMachineKt<*>) { - machinesPart.plusAssign(logicHost) - } - } + AEPatternRefresher.refreshAllPatternsAsync(grid) + .onStart { + isRefreshing = true + // 初始化进度条 + receiveFlowData(FlowData(0, 1)) } - machinesPart.forEach { s -> - runnable.add { - (0 until s.maxPatternCount).forEach { - if (!s.internalPatternInventory.getStackInSlot(it).isEmpty) { - s.onPatternChange(it) - } - } + .onEach { progress -> + receiveFlowData(FlowData(progress.completed, progress.total)) + } + .onCompletion { + isRefreshing = false + lastFlowData?.let { + receiveFlowData(FlowData(it.total, it.total)) } } - } - return runnable + .launchIn(uniqueScope!!) } - override fun buildCoroutine(run: List): CoroutineScope { - val scope = CoroutineScope(Dispatchers.Default) - flow { - val total = run.size - val block = 5 - run.chunked(block).forEachIndexed { blockIndex, runnables -> - runnables.forEach { it.run() } - val finished = ((blockIndex + 1) * block).coerceAtMost(total) - emit(FlowData(finished, total)) - delay(100) - } - }.onEach { - receiveFlowData(it) - }.onStart { - isRefreshing = true - }.onCompletion { - isRefreshing = false - }.launchIn(scope) - return scope + override fun receiveFlowData(flowData: FlowData) { + lastFlowData = flowData } } + val externalLogic by lazy { ExternalLogic() } abstract class ExternalLogicTemplate { abstract fun getAEKeyReplaced(stack: AEKey): AEKey } inner class ExternalLogic : ExternalLogicTemplate() { - private val lastAccessTime = Object2LongOpenHashMap() - - // 物品->首选物品映射表 - val itemToPreferred: ConcurrentHashMap = ConcurrentHashMap() - // 物品->物品排序从高到低映射表 - val itemToSort: Object2ObjectMap> = Object2ObjectOpenHashMap() - - override fun getAEKeyReplaced(stack: AEKey): AEKey { - itemToPreferred[stack]?.let { - lastAccessTime[stack] = System.currentTimeMillis() - return it - } - val sortStacks = itemToSort[stack] - if (sortStacks != null && sortStacks.isNotEmpty()) { - val preferred = sortStacks.first() - itemToPreferred[stack] = preferred - lastAccessTime[stack] = System.currentTimeMillis() - return preferred - } + @Volatile + private var substitutionMap = AEKeySubstitutionMap.EMPTY - itemToPreferred[stack] = stack - lastAccessTime[stack] = System.currentTimeMillis() - return stack - } + // TODO 使用PatternContentAccessTerminalPart获取(grid.getMachines(PatternContentAccessTerminalPart.class)) + override fun getAEKeyReplaced(stack: AEKey): AEKey = substitutionMap.getSubstitution(stack) fun fullyRefresh() { - itemToPreferred.clear() - itemToSort.clear() + val priorityGroups = mutableListOf>() + itemTransferList.transfers.forEach { transfer -> transfer as MyItemStackTransfer - val sortStacks = ObjectArrayList() + val priorityGroup = ObjectArrayList() val itemStacks = (0 until transfer.slots) .mapNotNull { transfer.getStackInSlot(it) } .filterNot { it.isEmpty } + var currentMode = ITEM + // 只要行内有一个流体容器,整行都按流体处理 for (stack in itemStacks) { - transfer.mode = ITEM stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).ifPresent { if (!it.drain(Int.MAX_VALUE, IFluidHandler.FluidAction.SIMULATE).isEmpty) { - transfer.mode = FLUID + currentMode = FLUID } } + if (currentMode == FLUID) break } + transfer.mode = currentMode + when (transfer.mode) { FLUID -> { val fluids = itemStacks .mapNotNull { stack -> - val capability = stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).resolve().orElse(null) ?: return@mapNotNull null - return@mapNotNull capability.drain(Int.MAX_VALUE, IFluidHandler.FluidAction.SIMULATE) + val cap = stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).resolve().orElse(null) + cap?.drain(Int.MAX_VALUE, IFluidHandler.FluidAction.SIMULATE) } .mapNotNull { AEFluidKey.of(it) } - sortStacks.addAll(fluids) - fluids.forEach { aek -> - itemToSort[aek] = sortStacks - } + priorityGroup.addAll(fluids) } ITEM -> { - val items = itemStacks - .mapNotNull { AEItemKey.of(it) } - sortStacks.addAll(items) - items.forEach { aek -> - itemToSort[aek] = sortStacks - } + val items = itemStacks.mapNotNull { AEItemKey.of(it) } + priorityGroup.addAll(items) } } + + if (priorityGroup.isNotEmpty()) { + priorityGroups.add(priorityGroup) + } } + // 原子性替换 + this.substitutionMap = AEKeySubstitutionMap(priorityGroups) } } diff --git a/src/main/java/com/gtocore/common/machine/multiblock/water/ClarifierPurificationUnitMachine.java b/src/main/java/com/gtocore/common/machine/multiblock/water/ClarifierPurificationUnitMachine.java index cdbcda36a..24a0c808b 100644 --- a/src/main/java/com/gtocore/common/machine/multiblock/water/ClarifierPurificationUnitMachine.java +++ b/src/main/java/com/gtocore/common/machine/multiblock/water/ClarifierPurificationUnitMachine.java @@ -36,7 +36,6 @@ @MethodsReturnNonnullByDefault public final class ClarifierPurificationUnitMachine extends WaterPurificationUnitMachine implements IFluidRendererMachine { - private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ClarifierPurificationUnitMachine.class); private static final Fluid AIR = GTMaterials.Air.getFluid(); @Persisted diff --git a/src/main/java/com/gtocore/utils/AEKeySubstitutionMap.kt b/src/main/java/com/gtocore/utils/AEKeySubstitutionMap.kt new file mode 100644 index 000000000..8289e892e --- /dev/null +++ b/src/main/java/com/gtocore/utils/AEKeySubstitutionMap.kt @@ -0,0 +1,48 @@ +package com.gtocore.utils + +import appeng.api.stacks.AEKey +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap + +import java.util.concurrent.ConcurrentHashMap + +class AEKeySubstitutionMap(priorityGroups: List>) { + + // 缓存:从任意Key映射到其首选Key,用于极速查询。 + private val keyToPreferredCache = ConcurrentHashMap() + + // 核心规则:从任意Key映射到其所在的整个优先级列表。 + private val keyToPriorityGroup: Map> + + init { + val mapBuilder = Object2ObjectOpenHashMap>() + priorityGroups.forEach { group -> + if (group.isNotEmpty()) { + group.forEach { key -> + mapBuilder[key] = group + } + } + } + this.keyToPriorityGroup = mapBuilder + } + + /** + * 获取给定AEKey的替换项(首选Key)。 + * + * @param key 需要查询的原始AEKey。 + * @return 替换后的首选AEKey。如果该Key没有定义替换规则,则返回其自身。 + */ + fun getSubstitution(key: AEKey): AEKey { + keyToPreferredCache[key]?.let { return it } + + val priorityGroup = keyToPriorityGroup[key] + + val preferredKey = priorityGroup?.firstOrNull() ?: key + + keyToPreferredCache[key] = preferredKey + return preferredKey + } + + companion object { + val EMPTY = AEKeySubstitutionMap(emptyList()) + } +} diff --git a/src/main/java/com/gtocore/utils/AEPatternRefresher.kt b/src/main/java/com/gtocore/utils/AEPatternRefresher.kt new file mode 100644 index 000000000..bbe74e5c2 --- /dev/null +++ b/src/main/java/com/gtocore/utils/AEPatternRefresher.kt @@ -0,0 +1,72 @@ +package com.gtocore.utils + +import com.gtocore.common.machine.multiblock.part.ae.MEPatternPartMachineKt + +import appeng.api.networking.IGrid +import appeng.blockentity.crafting.PatternProviderBlockEntity +import com.gregtechceu.gtceu.api.machine.MetaMachine +import com.gtolib.api.ae2.IExpandedGrid +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +data class RefreshProgress(val completed: Int, val total: Int) + +/** + * 一个用于异步刷新AE2网络中所有样板的工具类。 + * 这可以防止因一次性更新大量样板而导致的服务器卡顿。 + */ +object AEPatternRefresher { + + private const val TASK_CHUNK_SIZE = 10 // 每次处理的任务数量 + private const val DELAY_BETWEEN_CHUNKS_MS = 100L // 每批任务之间的延迟(毫秒) + + /** + * 异步刷新指定AE2网络中的所有样板提供者。 + * + * @param grid 目标AE2网络。 + * @return 一个Kotlin Flow,它会随刷新进度发出[RefreshProgress]状态。 + * 调用者可以收集(collect)这个Flow来更新UI,例如进度条。 + */ + fun refreshAllPatternsAsync(grid: IGrid): Flow = flow { + val refreshTasks = mutableListOf() + + grid.getActiveMachines(PatternProviderBlockEntity::class.java).forEach { + refreshTasks.add(Runnable { it.logic.updatePatterns() }) + } + + if (grid is IExpandedGrid) { + grid.machines.values() + .filter { it.isActive } + .mapNotNull { it.owner as? MEPatternPartMachineKt<*> } + .forEach { machine -> + refreshTasks.add( + Runnable { + (0 until machine.maxPatternCount).forEach { slotIndex -> + if (!machine.internalPatternInventory.getStackInSlot(slotIndex).isEmpty) { + machine.onPatternChange(slotIndex) + } + } + }, + ) + } + } + + val totalTasks = refreshTasks.size + if (totalTasks == 0) { + emit(RefreshProgress(0, 0)) + return@flow + } + + emit(RefreshProgress(0, totalTasks)) + + refreshTasks.chunked(TASK_CHUNK_SIZE).forEachIndexed { index, chunk -> + chunk.forEach { it.run() } + val completed = ((index + 1) * TASK_CHUNK_SIZE).coerceAtMost(totalTasks) + emit(RefreshProgress(completed, totalTasks)) + delay(DELAY_BETWEEN_CHUNKS_MS) + } + }.flowOn(Dispatchers.Default) +}