Skip to content

Commit a6e6549

Browse files
committed
- Implement parallel recipe process.
- Improve container access performance. - Update documents.
1 parent a0171d9 commit a6e6549

27 files changed

+1232
-837
lines changed

docs/API.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,19 @@ JEI/HEI 集成不属于 `PrototypeMachineryAPI` 的统一入口(它是典型
4141
详见:
4242

4343
- [生命周期与加载顺序](./Lifecycle.md)
44+
45+
## Key-level IO(基于 PMKey 的结构 IO)
46+
47+
PrototypeMachinery 的结构 IO(Structure IO)在内部已统一迁移为 **key-level**
48+
49+
- 使用 `PMKey<T>` + `Long` 数量作为核心语义
50+
- 扫描(parallelism 计算)与执行期复用同一套 key 匹配规则
51+
- 对外 capability(`IItemHandler` / `IFluidHandler`)仅作为边界适配层(因其 `Int` 限制会做分块/限幅)
52+
53+
接口位置:
54+
55+
- `src/main/kotlin/api/machine/component/container/StructureKeyContainers.kt`
56+
57+
文档与迁移说明:
58+
59+
- [Key-level IO(基于 PMKey 的 IO)](./KeyLevelIO.md)

docs/KeyLevelIO.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Key-level IO(基于 PMKey 的 IO)
2+
3+
本项目的结构 IO(Structure IO)在内部已统一迁移为 **key-level** 交互:以 `PMKey<T>` + `Long` 数量作为核心数据模型,而不是频繁构造/比较 `ItemStack` / `FluidStack`
4+
5+
This document describes the **key-level** IO model used internally by PrototypeMachinery: `PMKey<T>` + `Long` counts, instead of stack-level `ItemStack` / `FluidStack` churn.
6+
7+
## 目标 / Goals
8+
9+
- **性能**:减少 `ItemStack`/`FluidStack` 的构造、复制与 NBT 比较。
10+
- **一致性**:扫描(parallelism 计算)与执行期使用同一套 key 语义,避免“扫描忽略 tag / 执行考虑 tag”这类偏差。
11+
- **可扩展**:Addon 可以面向稳定的 key-level API 实现自定义容器/存储。
12+
13+
## API 位置 / Where is the API
14+
15+
相关接口位于:
16+
17+
- `src/main/kotlin/api/machine/component/container/StructureKeyContainers.kt`
18+
19+
主要接口:
20+
21+
- `StructureItemKeyContainer`
22+
- `StructureFluidKeyContainer`
23+
24+
它们是 **结构组件(StructureComponent)** 层级的容器视图:在结构成型/刷新时由结构解析生成,并存放在 `StructureComponentMap` 中。
25+
26+
## 基本语义 / Core semantics
27+
28+
### 以 Key + Long 为准
29+
30+
- Item:`PMKey<ItemStack>` + `Long`(数量)
31+
- Fluid:`PMKey<FluidStack>` + `Long`(mB 等计量)
32+
33+
这些接口提供形如:
34+
35+
- `insert(key, amount, action): Long`
36+
- `extract(key, amount, action): Long`
37+
38+
返回值为**实际插入/提取的数量**`Long`)。
39+
40+
### Action(EXECUTE / SIMULATE)
41+
42+
- `Action.SIMULATE`:只测算能插入/提取多少,不产生实际副作用。
43+
- `Action.EXECUTE`:真实执行。
44+
45+
注意:扫描并行约束一般使用 `SIMULATE`;配方执行使用 `EXECUTE`
46+
47+
## unchecked 方法(rollback 专用)
48+
49+
接口同时提供 unchecked 版本(命名可能为 `insertUnchecked` / `extractUnchecked`):
50+
51+
- 设计目的:**事务 rollback** 时恢复容器状态。
52+
- 语义:忽略 IOType(输入/输出)限制等“外部规则”,只做“把状态恢复回去”这件事。
53+
54+
重要:unchecked 不是给普通逻辑“绕过规则”用的。
55+
56+
## capability(Forge IItemHandler/IFluidHandler)适配策略
57+
58+
内部以 `Long` 计数运算,但 Forge capability 通常是 `Int` 数量(例如 `ItemStack.count``IFluidHandler.fill/drain`)。因此 cap-backed 容器需要在边界做分块/限幅:
59+
60+
- 统一采用 `Int.MAX_VALUE / 2` 作为安全上限(避免溢出、也避免某些实现对极端值的未定义行为)。
61+
- 大于该上限的插入/提取会被拆分为多个 chunk 循环完成。
62+
63+
这意味着:
64+
65+
- **对外 cap 只是兼容层**;如果你需要真正的大数与零分配语义,请使用 storage-backed(key-level storage)实现。
66+
67+
## 推荐用法 / Recommended usage
68+
69+
- 扫描/并行约束:只使用 `insert/extract(..., SIMULATE)`,不要 materialize stack。
70+
- 执行/事务:
71+
- commit 阶段用 `insert/extract(..., EXECUTE)`
72+
- rollback 阶段用 `insertUnchecked/extractUnchecked`(同样建议 EXECUTE)

src/main/kotlin/PrototypeMachinery.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import github.kasuminova.prototypemachinery.common.network.NetworkHandler
88
import github.kasuminova.prototypemachinery.common.registry.MachineTypeRegisterer
99
import github.kasuminova.prototypemachinery.common.structure.loader.StructureLoader
1010
import github.kasuminova.prototypemachinery.impl.recipe.index.RecipeIndexRegistry
11+
import github.kasuminova.prototypemachinery.impl.recipe.scanning.DefaultRecipeParallelismConstraints
1112
import github.kasuminova.prototypemachinery.impl.scheduler.TaskSchedulerImpl
1213
import github.kasuminova.prototypemachinery.integration.crafttweaker.CraftTweakerExamples
1314
import net.minecraftforge.common.MinecraftForge
@@ -55,6 +56,9 @@ public object PrototypeMachinery {
5556

5657
NetworkHandler.init()
5758

59+
// Register scan-time parallelism constraints (pluggable extension point for addons).
60+
DefaultRecipeParallelismConstraints.registerAll()
61+
5862
// Register scheduler to event bus
5963
// 注册调度器到事件总线
6064
MinecraftForge.EVENT_BUS.register(TaskSchedulerImpl)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package github.kasuminova.prototypemachinery.api.machine.component.container
2+
3+
import github.kasuminova.prototypemachinery.api.key.PMKey
4+
import github.kasuminova.prototypemachinery.api.machine.component.StructureComponent
5+
import github.kasuminova.prototypemachinery.common.util.Action
6+
import github.kasuminova.prototypemachinery.common.util.IOType
7+
import net.minecraft.item.ItemStack
8+
import net.minecraftforge.fluids.FluidStack
9+
10+
/**
11+
* Key-level container view for items.
12+
*
13+
* This is the preferred API for recipe IO and scanning.
14+
*
15+
* - No ItemStack predicate matching
16+
* - No per-slot snapshot/restore requirements
17+
* - All amounts are Long
18+
*/
19+
public interface StructureItemKeyContainer : StructureComponent {
20+
21+
public fun isAllowedIOType(ioType: IOType): Boolean
22+
23+
/** Inserts up to [amount] of [key]. Returns the amount actually inserted. */
24+
public fun insert(key: PMKey<ItemStack>, amount: Long, action: Action): Long
25+
26+
/** Extracts up to [amount] of [key]. Returns the amount actually extracted. */
27+
public fun extract(key: PMKey<ItemStack>, amount: Long, action: Action): Long
28+
29+
/** Unchecked variant that ignores IOType restrictions (for rollback). */
30+
public fun insertUnchecked(key: PMKey<ItemStack>, amount: Long, action: Action): Long
31+
32+
/** Unchecked variant that ignores IOType restrictions (for rollback). */
33+
public fun extractUnchecked(key: PMKey<ItemStack>, amount: Long, action: Action): Long
34+
}
35+
36+
/**
37+
* Key-level container view for fluids.
38+
*
39+
* NOTE: fluid key equality decides whether NBT/tag participates.
40+
*/
41+
public interface StructureFluidKeyContainer : StructureComponent {
42+
43+
public fun isAllowedIOType(ioType: IOType): Boolean
44+
45+
/** Inserts up to [amount] of [key]. Returns the amount actually inserted. */
46+
public fun insert(key: PMKey<FluidStack>, amount: Long, action: Action): Long
47+
48+
/** Extracts up to [amount] of [key]. Returns the amount actually extracted. */
49+
public fun extract(key: PMKey<FluidStack>, amount: Long, action: Action): Long
50+
51+
/** Unchecked variant that ignores IOType restrictions (for rollback). */
52+
public fun insertUnchecked(key: PMKey<FluidStack>, amount: Long, action: Action): Long
53+
54+
/** Unchecked variant that ignores IOType restrictions (for rollback). */
55+
public fun extractUnchecked(key: PMKey<FluidStack>, amount: Long, action: Action): Long
56+
}

src/main/kotlin/api/recipe/requirement/component/system/RecipeRequirementSystem.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ public interface RecipeRequirementSystem<C : RecipeRequirementComponent> {
120120
*/
121121
public interface RequirementTransaction {
122122

123+
/**
124+
* A shared no-op success transaction instance.
125+
*
126+
* 通用的“成功且无副作用”的事务单例,用于避免在各处反复构造匿名对象。
127+
*
128+
* - result = [ProcessResult.Success]
129+
* - commit()/rollback() 均为 no-op
130+
*/
131+
public object NoOpSuccess : RequirementTransaction {
132+
override val result: ProcessResult = ProcessResult.Success
133+
override fun commit() {}
134+
override fun rollback() {}
135+
}
136+
123137
/** Result of attempting to acquire the transaction / 获取事务的结果 */
124138
public val result: ProcessResult
125139

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package github.kasuminova.prototypemachinery.api.recipe.scanning
2+
3+
import github.kasuminova.prototypemachinery.api.machine.MachineInstance
4+
import github.kasuminova.prototypemachinery.api.recipe.MachineRecipe
5+
import github.kasuminova.prototypemachinery.api.recipe.requirement.component.RecipeRequirementComponent
6+
import net.minecraft.util.ResourceLocation
7+
8+
/**
9+
* A pluggable constraint used by the recipe scanning system to decide whether a recipe can run with a given
10+
* effective parallel amount `k`.
11+
*
12+
* 用于“配方扫描阶段”计算最大可并行数 k 的可插拔约束。
13+
*
14+
* ### Design notes
15+
* - The scanning system will binary-search k in [1..limit].
16+
* - Each constraint decides whether the machine state can satisfy the requirement(s) at that k, typically via
17+
* Action.SIMULATE.
18+
* - If a requirement type has no registered constraint, the scanner will conservatively clamp k to 1.
19+
*/
20+
public interface RecipeParallelismConstraint {
21+
22+
/** Requirement type id this constraint applies to. / 约束对应的需求类型 ID */
23+
public val requirementTypeId: ResourceLocation
24+
25+
/**
26+
* Optional fast upper bound estimation.
27+
*
28+
* You may return a value <= currentLimit to reduce the binary search space.
29+
*
30+
* 可选的上界估算:用于快速收敛二分范围。
31+
*/
32+
public fun upperBound(
33+
machine: MachineInstance,
34+
recipe: MachineRecipe,
35+
components: List<RecipeRequirementComponent>,
36+
currentLimit: Int
37+
): Int = currentLimit
38+
39+
/**
40+
* Whether the machine can satisfy the given requirement components at the specified parallel amount.
41+
*
42+
* Return false to indicate the scanner should try a smaller k.
43+
*/
44+
public fun canSatisfy(
45+
machine: MachineInstance,
46+
recipe: MachineRecipe,
47+
components: List<RecipeRequirementComponent>,
48+
parallels: Int
49+
): Boolean
50+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package github.kasuminova.prototypemachinery.api.recipe.scanning
2+
3+
import net.minecraft.util.ResourceLocation
4+
import java.util.concurrent.ConcurrentHashMap
5+
6+
/**
7+
* Registry for [RecipeParallelismConstraint].
8+
*
9+
* 配方并行可行性约束注册表。
10+
*
11+
* This is intentionally minimal:
12+
* - The core mod registers default constraints for built-in requirement types.
13+
* - Addons can register constraints for their custom requirement types to participate in scan-time parallelism.
14+
*/
15+
public object RecipeParallelismConstraintRegistry {
16+
17+
private val constraints: MutableMap<ResourceLocation, RecipeParallelismConstraint> = ConcurrentHashMap()
18+
19+
@JvmStatic
20+
public fun register(constraint: RecipeParallelismConstraint): RecipeParallelismConstraint {
21+
constraints[constraint.requirementTypeId] = constraint
22+
return constraint
23+
}
24+
25+
@JvmStatic
26+
public fun get(requirementTypeId: ResourceLocation): RecipeParallelismConstraint? {
27+
return constraints[requirementTypeId]
28+
}
29+
30+
@JvmStatic
31+
public fun all(): Collection<RecipeParallelismConstraint> {
32+
return constraints.values
33+
}
34+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package github.kasuminova.prototypemachinery.common.util
2+
3+
import github.kasuminova.prototypemachinery.api.machine.attribute.StandardMachineAttributes
4+
import github.kasuminova.prototypemachinery.api.recipe.process.RecipeProcess
5+
import kotlin.math.floor
6+
7+
/**
8+
* Utilities for recipe parallelism.
9+
*
10+
* IMPORTANT:
11+
* - We treat PROCESS_PARALLELISM on the process attribute map as the **effective** parallel amount
12+
* for this specific process instance.
13+
* - The value is floored to an integer and clamped to >= 1.
14+
*/
15+
public object RecipeParallelism {
16+
17+
public fun getParallelism(process: RecipeProcess): Int {
18+
val raw = process.attributeMap.attributes[StandardMachineAttributes.PROCESS_PARALLELISM]?.value ?: 1.0
19+
if (raw.isNaN() || raw.isInfinite()) return 1
20+
return floor(raw).toInt().coerceAtLeast(1)
21+
}
22+
23+
/**
24+
* Scales a non-negative [base] by integer [k] with overflow protection.
25+
*
26+
* If overflow would occur, this returns [Long.MAX_VALUE] (saturating arithmetic).
27+
*/
28+
public fun scaleCount(base: Long, k: Int): Long {
29+
if (base <= 0L) return base
30+
if (k <= 1) return base
31+
if (base > Long.MAX_VALUE / k.toLong()) return Long.MAX_VALUE
32+
return base * k.toLong()
33+
}
34+
}
35+
36+
/** Shortcut extension: effective parallels for this process (>= 1). */
37+
public fun RecipeProcess.parallelism(): Int = RecipeParallelism.getParallelism(this)
38+
39+
/** Shortcut extension: scale count by this process's parallelism. */
40+
public fun RecipeProcess.scaleByParallelism(base: Long): Long = RecipeParallelism.scaleCount(base, parallelism())

0 commit comments

Comments
 (0)