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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
apply from: "$projectDir/dependencies.gradle"

// Exercise Create's real network accounting in the owning module, independently of the aggregate modpack.
sourceSets.test {
compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath
runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath
}

legacyForge {
addModdingDependenciesTo(sourceSets.test)
mods.named(project.mod_id) {
sourceSet(sourceSets.test)
}
runs {
gameTestServer {
type = "gameTestServer"
sourceSet = sourceSets.test
gameDirectory.set(file('run/gametest'))
systemProperty('forge.enabledGameTestNamespaces', project.mod_id)
logLevel = org.slf4j.event.Level.INFO
}
}
}

tasks.named('test', Test).configure {
// These tests run in Forge's GameTestServer rather than the JUnit runner.
failOnNoDiscoveredTests = false
}

tasks.named('processTestResources').configure {
// Reuse the existing empty GameTest template; no GTM test classes are loaded by this run.
from(project(':modules:GregTech-Modern').file('src/test/resources/data/gtceu/structures/empty.nbt')) {
into 'data/ctpp/structures'
}
}
127 changes: 111 additions & 16 deletions src/main/java/com/mo_guang/ctpp/api/pattern/StaticBlockPattern.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,53 @@
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;

import com.mo_guang.ctpp.dynamicPart.rotation.IContraptionMultiblock;
import com.simibubi.create.content.kinetics.base.KineticBlock;
import com.simibubi.create.content.processing.burner.BlazeBurnerBlock;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.jetbrains.annotations.Nullable;

import java.util.*;

public class StaticBlockPattern extends BlockPattern {

private static final String DYNAMIC_POSITIONS = "ctppDynamicPositions";

protected final boolean[][][] staticBlockMatches;
protected final int[][][] dynamicBlockMatches;
private final int dynamicGroupCount;

public StaticBlockPattern(TraceabilityPredicate[][][] predicatesIn, RelativeDirection[] structureDir,
int[][] aisleRepetitions, int[] centerOffset,
boolean[][][] staticPredicates, int[][][] dynamicPredicates) {
super(predicatesIn, structureDir, aisleRepetitions, centerOffset);
this.staticBlockMatches = staticPredicates;
this.dynamicBlockMatches = dynamicPredicates;
var dynamicGroups = new IntOpenHashSet();
for (int c = 0; c < staticPredicates.length; c++) {
for (int b = 0; b < staticPredicates[c].length; b++) {
for (int a = 0; a < staticPredicates[c][b].length; a++) {
if (!staticPredicates[c][b][a]) {
dynamicGroups.add(dynamicPredicates[c][b][a]);
}
}
}
}
this.dynamicGroupCount = dynamicGroups.size();
}

@Override
public boolean checkPatternAt(MultiblockState worldState, boolean savePredicate) {
IMultiController controller = worldState.getController();
if (controller == null) {
worldState.setError(new PatternStringError("no controller found"));
if (!worldState.world.isLoaded(worldState.controllerPos)) {
worldState.setError(MultiblockState.UNLOAD_ERROR);
} else {
worldState.setError(new PatternStringError("no controller found"));
}
return false;
}
BlockPos centerPos = controller.self().getPos();
Expand All @@ -49,14 +73,32 @@ public boolean checkPatternAt(MultiblockState worldState, boolean savePredicate)
new Direction[] { Direction.SOUTH, Direction.NORTH, Direction.EAST, Direction.WEST };
Direction upwardsFacing = controller.self().getUpwardsFacing();
boolean allowsFlip = controller.self().allowFlip();
boolean preserveOwnedFlip = allowsFlip && controller.isStructureFormedSnapshot();
boolean preferredFlip = preserveOwnedFlip && controller.isStructureFlippedSnapshot();
boolean sawUnloadedPosition = false;
for (Direction direction : facings) {
boolean result = checkPatternAt(worldState, centerPos, direction, upwardsFacing, false, savePredicate);
if (result) {
if (checkPatternAt(worldState, centerPos, direction, upwardsFacing, preferredFlip, savePredicate)) {
return true;
} else if (allowsFlip) {
return checkPatternAt(worldState, centerPos, direction, upwardsFacing, true, savePredicate);
}
sawUnloadedPosition |= worldState.error == MultiblockState.UNLOAD_ERROR;
if (allowsFlip) {
if (preserveOwnedFlip && sawUnloadedPosition) {
worldState.setError(MultiblockState.UNLOAD_ERROR);
return false;
}
if (checkPatternAt(worldState, centerPos, direction, upwardsFacing, !preferredFlip, savePredicate)) {
return true;
}
sawUnloadedPosition |= worldState.error == MultiblockState.UNLOAD_ERROR;
if (sawUnloadedPosition) {
worldState.setError(MultiblockState.UNLOAD_ERROR);
}
return false;
}
}
if (sawUnloadedPosition) {
worldState.setError(MultiblockState.UNLOAD_ERROR);
}
return false;
}

Expand Down Expand Up @@ -86,14 +128,54 @@ public Map<Integer, List<BlockPos>> getDynamicPart(MultiblockState worldState) {
return parts;
}

/**
* Tests the geometry cached by the last pattern scan. Contraption assembly produces many block updates in a
* burst, so rebuilding every dynamic group and linearly searching every list for each update makes large static
* patterns quadratic. The fallback only serves legacy callers whose context predates this cache.
*/
public boolean isDynamicPosition(MultiblockState worldState, BlockPos pos) {
LongSet positions = getCachedDynamicPositions(worldState);
if (positions == null) {
positions = new LongOpenHashSet();
for (List<BlockPos> group : getDynamicPart(worldState).values()) {
for (BlockPos groupPos : group) {
positions.add(groupPos.asLong());
}
}
worldState.getMatchContext().set(DYNAMIC_POSITIONS, positions);
}
return positions.contains(pos.asLong());
}

/** The scan-owned lookup; callers must not mutate it. Null means no static pattern has populated this context. */
@Nullable
public static LongSet getCachedDynamicPositions(MultiblockState worldState) {
return worldState.getMatchContext().get(DYNAMIC_POSITIONS);
}

@Override
public boolean checkPatternAt(MultiblockState worldState, BlockPos centerPos, Direction frontFacing,
Direction upwardsFacing, boolean isFlipped, boolean savePredicate) {
IMultiController controller = worldState.getController();
boolean findFirstAisle = false;
int minZ = -centerOffset[4];
worldState.clean();
boolean skipAssembledDynamic = false;
if (controller.isStructureFormedSnapshot() && dynamicGroupCount > 0 &&
controller instanceof IContraptionMultiblock<?> contraptionController) {
if (contraptionController.hasCompleteAttachedContraption(dynamicGroupCount)) {
skipAssembledDynamic = true;
} else if (!contraptionController.isAssemblyPivotEntityTicking()) {
// Absence is not evidence while the chunk which owns the contraption entity is unavailable or has not
// reached entity-ticking status. Keep the formed snapshot pending and retry after loading settles.
worldState.setError(MultiblockState.UNLOAD_ERROR);
return false;
}
// If the pivot is fully ticking and no complete entity set exists, validate the original blocks. A
// disassembled structure can then reform; missing entities plus missing source blocks is definitive.
}
PatternMatchContext matchContext = worldState.getMatchContext();
matchContext.getOrCreate(DYNAMIC_POSITIONS, LongOpenHashSet::new);
Object2IntMap<SimplePredicate> globalCount = worldState.getGlobalCount();
Object2IntMap<SimplePredicate> layerCount = worldState.getLayerCount();
// Checking aisles
Expand All @@ -108,30 +190,33 @@ public boolean checkPatternAt(MultiblockState worldState, BlockPos centerPos, Di
for (int b = 0, y = -centerOffset[1]; b < this.thumbLength; b++, y++) {
for (int a = 0, x = -centerOffset[0]; a < this.palmLength; a++, x++) {
worldState.setError(null);
TraceabilityPredicate predicate;
if (!controller.isFormed()) {
predicate = blockMatches[c][b][a];
} else {
predicate = this.staticBlockMatches[c][b][a] ? this.blockMatches[c][b][a] :
Predicates.any();
}
TraceabilityPredicate declaredPredicate = blockMatches[c][b][a];
boolean dynamicPosition = !staticBlockMatches[c][b][a];
TraceabilityPredicate predicate = skipAssembledDynamic && dynamicPosition ?
Predicates.any() : declaredPredicate;
BlockPos pos = setActualRelativeOffset(x, y, z, frontFacing, upwardsFacing, isFlipped)
.offset(centerPos.getX(), centerPos.getY(), centerPos.getZ());
if (!worldState.update(pos, predicate)) {
return false;
}
if (predicate.addCache()) {
worldState.addPosCache(pos, predicate);
if (dynamicPosition) {
matchContext.getOrCreate(DYNAMIC_POSITIONS, LongOpenHashSet::new).add(pos.asLong());
}
// Dynamic blocks are replaced by contraption entities after formation. Continue validating
// them as ANY, but retain their declared predicate in the confirmed geometry so chunk-unload
// preflight and reverse chunk indexing still cover the complete owned structure.
if (declaredPredicate.addCache() || dynamicPosition) {
worldState.addPosCache(pos, declaredPredicate);
if (savePredicate) {
matchContext.getOrCreate("predicates", HashMap::new).put(pos, predicate);
matchContext.getOrCreate("predicates", HashMap::new).put(pos, declaredPredicate);
}
}
boolean canPartShared = true;
if (worldState.getTileEntity() instanceof IMachineBlockEntity machineBlockEntity &&
machineBlockEntity.getMetaMachine() instanceof IMultiPart part) { // add detected parts
if (!predicate.isAny()) {
if (part.isFormed() && !part.canShared() &&
!part.hasController(worldState.controllerPos)) { // check part can be shared
!part.hasController(controller)) { // check part can be shared
canPartShared = false;
worldState.setError(new PatternStringError("multiblocked.pattern.error.share"));
} else {
Expand All @@ -143,6 +228,16 @@ public boolean checkPatternAt(MultiblockState worldState, BlockPos centerPos, Di
matchContext.getOrCreate("vaBlocks", LongOpenHashSet::new)
.add(worldState.getPos().asLong());
}
// StaticBlockPattern overrides BlockPattern's scan method, so CTPP's BlockPattern mixin does
// not run here. Collect these ownership surfaces explicitly before the predicate test.
if (!dynamicPosition && worldState.getBlockState().getBlock() instanceof KineticBlock) {
matchContext.getOrCreate("roBlocks", LongOpenHashSet::new)
.add(worldState.getPos().asLong());
}
if (!dynamicPosition && worldState.getBlockState().getBlock() instanceof BlazeBurnerBlock) {
matchContext.getOrCreate("bbBlocks", LongOpenHashSet::new)
.add(worldState.getPos().asLong());
}
if (!predicate.test(worldState) || !canPartShared) { // matching failed
if (findFirstAisle) {
if (r < aisleRepetitions[c][0]) {// retreat to see if the first aisle can start later
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.mo_guang.ctpp.common.blockentity;

import net.minecraft.core.BlockPos;

public interface IKineticBlockEntityExtension {

// 设置是否在多方块中的状态
void setCTNHInMultiblock(boolean inMultiblock);
void ctpp$claimMultiblockOwner(BlockPos controllerPos, long instanceId, float visualSpeed);

void setCTNHVisualSpeed(float speed);
void ctpp$releaseMultiblockOwner(BlockPos controllerPos, long instanceId);
}
Loading
Loading