package com.xfestudio.mydimension.client.builder;
import com.xfestudio.mydimension.builder.BuilderMode;
import com.xfestudio.mydimension.builder.BuilderBlockCompatibility;
import com.xfestudio.mydimension.builder.BuilderInteractionPolicy;
import com.xfestudio.mydimension.builder.BuilderReplacementPolicy;
import com.xfestudio.mydimension.builder.BuilderTags;
import com.xfestudio.mydimension.builder.SurfacePlaneTraversal;
import com.xfestudio.mydimension.builder.SurfaceMatchMode;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import java.util.ArrayList;
import java.util.List;
/** Lightweight client prediction; the server still recomputes and authorizes every target. */
public final class BuilderSurfacePreviewPlanner {
private static final int VALIDATION_CELLS_PER_TICK = 256;
private static final int TARGET_MISS_GRACE_TICKS = 2;
private static PreviewKey lastKey;
private static boolean showingSurfacePreview;
private static BuilderPreviewState.Snapshot lastPlannedSnapshot;
private static List<BuilderPreviewState.Cell> lastCells = List.of();
private static int validationCursor;
private static Direction lastFace;
private static BlockState lastOverride;
private static int targetMissTicks;
private BuilderSurfacePreviewPlanner() { }
public static void update(Minecraft minecraft) {
if (minecraft.player == null || minecraft.level == null) return;
BuilderPreviewState.Snapshot workflow = BuilderPreviewState.get().snapshot();
// A movable deployment owns the placement target. Source selections and material-waiting
// jobs are independent overlays and must not suppress ordinary surface prediction.
if (workflow != null && workflow.blueprintPreview()) {
reset();
return;
}
BuilderClientSnapshot settings = BuilderClientServices.snapshot();
HitResult picked = minecraft.player.pick(settings.reach(), 1.0F, false);
if (!(picked instanceof BlockHitResult hit) || picked.getType() != HitResult.Type.BLOCK) {
if (showingSurfacePreview && ++targetMissTicks < TARGET_MISS_GRACE_TICKS) return;
if (showingSurfacePreview) BuilderPreviewState.get().clearSurfacePreview();
reset();
return;
}
targetMissTicks = 0;
BlockState targetState = minecraft.level.getBlockState(hit.getBlockPos());
if (!BuilderInteractionPolicy.permitsScepter(Screen.hasShiftDown(), targetState)) {
// Interaction-priority targets hide a previous surface preview immediately. The
// ordinary right-click path is then free to reach the block itself.
BuilderPreviewState.get().clearSurfacePreview();
reset();
return;
}
BlockState override = minecraft.player.getOffhandItem().getItem() instanceof BlockItem blockItem
? blockItem.getBlock().defaultBlockState() : null;
PreviewKey key = new PreviewKey(minecraft.level, hit.getBlockPos().immutable(), hit.getDirection(),
targetState, settings.mode(), settings.surfaceMatch(),
settings.buildLimit(), settings.demolishLimit(), settings.allowReplacement(),
minecraft.player.isCreative(), override,
minecraft.level.getBlockState(hit.getBlockPos().relative(hit.getDirection())));
boolean snapshotReplaced = showingSurfacePreview && workflow != lastPlannedSnapshot;
if (key.equals(lastKey) && !snapshotReplaced
&& !hasPreviewDrift(minecraft.level, key)) return;
List<BuilderPreviewState.Cell> cells = plan(minecraft.level, hit, settings, override);
BuilderPreviewState.get().updateSurfacePreview(minecraft.level.dimension(), cells);
lastKey = key;
showingSurfacePreview = true;
lastPlannedSnapshot = BuilderPreviewState.get().snapshot();
lastCells = cells;
validationCursor = 0;
lastFace = hit.getDirection();
lastOverride = override;
}
public static void reset() {
lastKey = null;
showingSurfacePreview = false;
lastPlannedSnapshot = null;
lastCells = List.of();
validationCursor = 0;
lastFace = null;
lastOverride = null;
targetMissTicks = 0;
}
private static List<BuilderPreviewState.Cell> plan(ClientLevel level, BlockHitResult hit,
BuilderClientSnapshot settings,
BlockState override) {
BlockState seed = level.getBlockState(hit.getBlockPos());
if (!SurfacePlaneTraversal.isReference(seed)
|| !isVisibleBuildReference(level, hit.getBlockPos(), hit.getDirection(), seed,
settings.mode(), settings.allowReplacement())) return List.of();
BuilderMode mode = settings.mode();
boolean creative = Minecraft.getInstance().player != null
&& Minecraft.getInstance().player.isCreative();
int limit = mode == BuilderMode.BUILD ? settings.buildLimit() : settings.demolishLimit();
int scanLimit = Math.min(65_536, Math.max(limit, limit * 16));
SurfacePlaneTraversal.Result traversal = SurfacePlaneTraversal.traverse(
hit.getBlockPos(), hit.getDirection().getAxis(), scanLimit, limit,
pos -> {
BlockState state = level.getBlockState(pos);
return SurfacePlaneTraversal.isReference(state)
&& (settings.surfaceMatch() != SurfaceMatchMode.SAME_BLOCK
|| BuilderBlockCompatibility.sameSurfaceType(seed, state))
&& isVisibleBuildReference(level, pos, hit.getDirection(), state,
mode, settings.allowReplacement());
}, pos -> {
if (mode == BuilderMode.DEMOLISH) return true;
BlockPos target = pos.relative(hit.getDirection());
if (target.getY() < level.getMinBuildHeight()
|| target.getY() >= level.getMaxBuildHeight()
|| !level.getWorldBorder().isWithinBounds(target)) return false;
BlockState source = level.getBlockState(pos);
BlockState desired = override == null ? source : override;
BlockState current = level.getBlockState(target);
// Match SurfacePlanner's operation-budget predicate. Already-matching cells are
// useful context, but must not consume a slot and hide a later executable cell.
return !current.equals(desired)
&& (current.canBeReplaced() || settings.allowReplacement());
});
List<BuilderPreviewState.Cell> result = new ArrayList<>(traversal.nodes().size());
for (SurfacePlaneTraversal.Node node : traversal.nodes()) {
BlockState source = level.getBlockState(node.pos());
if (mode == BuilderMode.DEMOLISH) {
BuilderPreviewState.Kind kind = source.is(BuilderTags.CONSTRUCTION_PROTECTED)
|| source.is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.INVALID : BuilderPreviewState.Kind.DEMOLISH;
result.add(new BuilderPreviewState.Cell(node.pos(), source, kind, false));
} else {
BlockPos target = node.pos().relative(hit.getDirection());
BlockState desired = override == null ? source : override;
BlockState current = level.getBlockState(target);
BuilderPreviewState.Kind kind = current.equals(desired)
? BuilderPreviewState.Kind.BLUEPRINT
: BuilderReplacementPolicy.canReplaceTarget(current, level, target,
settings.allowReplacement(), creative) && desired.canSurvive(level, target)
&& !desired.is(BuilderTags.CONSTRUCTION_PROTECTED)
&& !desired.is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.BUILD : BuilderPreviewState.Kind.INVALID;
result.add(new BuilderPreviewState.Cell(target, desired, kind,
kind == BuilderPreviewState.Kind.BUILD));
}
}
return List.copyOf(result);
}
/** Checks a bounded slice of the previous result and only reruns BFS after observable drift. */
private static boolean hasPreviewDrift(ClientLevel level, PreviewKey key) {
if (lastCells.isEmpty() || lastFace == null) return false;
int checks = Math.min(VALIDATION_CELLS_PER_TICK, lastCells.size());
for (int count = 0; count < checks; count++) {
if (validationCursor >= lastCells.size()) validationCursor = 0;
BuilderPreviewState.Cell cell = lastCells.get(validationCursor++);
BlockState current = level.getBlockState(cell.pos());
if (key.mode() == BuilderMode.DEMOLISH) {
if (!SurfacePlaneTraversal.hasExposedReferenceFace(
level, cell.pos(), lastFace)) return true;
BuilderPreviewState.Kind currentKind = current.is(BuilderTags.CONSTRUCTION_PROTECTED)
|| current.is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.INVALID : BuilderPreviewState.Kind.DEMOLISH;
if (!current.equals(cell.state()) || currentKind != cell.kind()) return true;
continue;
}
BuilderPreviewState.Kind currentKind = current.equals(cell.state())
? BuilderPreviewState.Kind.BLUEPRINT
: BuilderReplacementPolicy.canReplaceTarget(current, level, cell.pos(),
key.allowReplacement(), key.creative())
&& cell.state().canSurvive(level, cell.pos())
&& !cell.state().is(BuilderTags.CONSTRUCTION_PROTECTED)
&& !cell.state().is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.BUILD : BuilderPreviewState.Kind.INVALID;
if (currentKind != cell.kind()) return true;
BlockState source = level.getBlockState(cell.pos().relative(lastFace.getOpposite()));
if (!isVisibleBuildReference(level, cell.pos().relative(lastFace.getOpposite()),
lastFace, source, key.mode(), key.allowReplacement())) return true;
if (lastOverride == null) {
if (!source.equals(cell.state())) return true;
} else if (!SurfacePlaneTraversal.isReference(source)
|| key.match() == SurfaceMatchMode.SAME_BLOCK
&& !BuilderBlockCompatibility.sameSurfaceType(key.seedState(), source)) {
return true;
}
}
return false;
}
/** Client mirror of SurfacePlanner's visible reference/replacement surface boundary. */
private static boolean isVisibleBuildReference(ClientLevel level, BlockPos referencePos,
Direction face, BlockState reference,
BuilderMode mode, boolean allowReplacement) {
if (SurfacePlaneTraversal.hasExposedReferenceFace(
level, referencePos, face, reference)) return true;
if (mode != BuilderMode.BUILD || !allowReplacement) return false;
BlockPos target = referencePos.relative(face);
BlockState obstacle = level.getBlockState(target);
return BuilderReplacementPolicy.requiresBreaking(obstacle)
&& SurfacePlaneTraversal.hasExposedReferenceFace(level, target, face, obstacle);
}
private record PreviewKey(ClientLevel level, BlockPos seed, Direction face, BlockState seedState,
BuilderMode mode, SurfaceMatchMode match, int buildLimit,
int demolishLimit, boolean allowReplacement, boolean creative,
BlockState override, BlockState targetSeedState) { }
}
package com.xfestudio.mydimension.client.builder;
import com.xfestudio.mydimension.builder.BuilderMode;
import com.xfestudio.mydimension.builder.BuilderBlockCompatibility;
import com.xfestudio.mydimension.builder.BuilderInteractionPolicy;
import com.xfestudio.mydimension.builder.BuilderReplacementPolicy;
import com.xfestudio.mydimension.builder.BuilderTags;
import com.xfestudio.mydimension.builder.SurfacePlaneTraversal;
import com.xfestudio.mydimension.builder.SurfaceMatchMode;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import java.util.ArrayList;
import java.util.List;
/** Lightweight client prediction; the server still recomputes and authorizes every target. */
public final class BuilderSurfacePreviewPlanner {
private static final int VALIDATION_CELLS_PER_TICK = 256;
private static final int TARGET_MISS_GRACE_TICKS = 2;
private static PreviewKey lastKey;
private static boolean showingSurfacePreview;
private static BuilderPreviewState.Snapshot lastPlannedSnapshot;
private static List<BuilderPreviewState.Cell> lastCells = List.of();
private static int validationCursor;
private static Direction lastFace;
private static BlockState lastOverride;
private static int targetMissTicks;
private BuilderSurfacePreviewPlanner() { }
public static void update(Minecraft minecraft) {
if (minecraft.player == null || minecraft.level == null) return;
BuilderPreviewState.Snapshot workflow = BuilderPreviewState.get().snapshot();
// A movable deployment owns the placement target. Source selections and material-waiting
// jobs are independent overlays and must not suppress ordinary surface prediction.
if (workflow != null && workflow.blueprintPreview()) {
reset();
return;
}
BuilderClientSnapshot settings = BuilderClientServices.snapshot();
HitResult picked = minecraft.player.pick(settings.reach(), 1.0F, false);
if (!(picked instanceof BlockHitResult hit) || picked.getType() != HitResult.Type.BLOCK) {
if (showingSurfacePreview && ++targetMissTicks < TARGET_MISS_GRACE_TICKS) return;
if (showingSurfacePreview) BuilderPreviewState.get().clearSurfacePreview();
reset();
return;
}
targetMissTicks = 0;
BlockState targetState = minecraft.level.getBlockState(hit.getBlockPos());
if (!BuilderInteractionPolicy.permitsScepter(Screen.hasShiftDown(), targetState)) {
// Interaction-priority targets hide a previous surface preview immediately. The
// ordinary right-click path is then free to reach the block itself.
BuilderPreviewState.get().clearSurfacePreview();
reset();
return;
}
BlockState override = minecraft.player.getOffhandItem().getItem() instanceof BlockItem blockItem
? blockItem.getBlock().defaultBlockState() : null;
PreviewKey key = new PreviewKey(minecraft.level, hit.getBlockPos().immutable(), hit.getDirection(),
targetState, settings.mode(), settings.surfaceMatch(),
settings.buildLimit(), settings.demolishLimit(), settings.allowReplacement(),
minecraft.player.isCreative(), override,
minecraft.level.getBlockState(hit.getBlockPos().relative(hit.getDirection())));
boolean snapshotReplaced = showingSurfacePreview && workflow != lastPlannedSnapshot;
if (key.equals(lastKey) && !snapshotReplaced
&& !hasPreviewDrift(minecraft.level, key)) return;
List<BuilderPreviewState.Cell> cells = plan(minecraft.level, hit, settings, override);
BuilderPreviewState.get().updateSurfacePreview(minecraft.level.dimension(), cells);
lastKey = key;
showingSurfacePreview = true;
lastPlannedSnapshot = BuilderPreviewState.get().snapshot();
lastCells = cells;
validationCursor = 0;
lastFace = hit.getDirection();
lastOverride = override;
}
public static void reset() {
lastKey = null;
showingSurfacePreview = false;
lastPlannedSnapshot = null;
lastCells = List.of();
validationCursor = 0;
lastFace = null;
lastOverride = null;
targetMissTicks = 0;
}
private static List<BuilderPreviewState.Cell> plan(ClientLevel level, BlockHitResult hit,
BuilderClientSnapshot settings,
BlockState override) {
BlockState seed = level.getBlockState(hit.getBlockPos());
if (!SurfacePlaneTraversal.isReference(seed)
|| !isVisibleBuildReference(level, hit.getBlockPos(), hit.getDirection(), seed,
settings.mode(), settings.allowReplacement())) return List.of();
BuilderMode mode = settings.mode();
boolean creative = Minecraft.getInstance().player != null
&& Minecraft.getInstance().player.isCreative();
int limit = mode == BuilderMode.BUILD ? settings.buildLimit() : settings.demolishLimit();
int scanLimit = Math.min(65_536, Math.max(limit, limit * 16));
SurfacePlaneTraversal.Result traversal = SurfacePlaneTraversal.traverse(
hit.getBlockPos(), hit.getDirection().getAxis(), scanLimit, limit,
pos -> {
BlockState state = level.getBlockState(pos);
return SurfacePlaneTraversal.isReference(state)
&& (settings.surfaceMatch() != SurfaceMatchMode.SAME_BLOCK
|| BuilderBlockCompatibility.sameSurfaceType(seed, state))
&& isVisibleBuildReference(level, pos, hit.getDirection(), state,
mode, settings.allowReplacement());
}, pos -> {
if (mode == BuilderMode.DEMOLISH) return true;
BlockPos target = pos.relative(hit.getDirection());
if (target.getY() < level.getMinBuildHeight()
|| target.getY() >= level.getMaxBuildHeight()
|| !level.getWorldBorder().isWithinBounds(target)) return false;
BlockState source = level.getBlockState(pos);
BlockState desired = override == null ? source : override;
BlockState current = level.getBlockState(target);
// Match SurfacePlanner's operation-budget predicate. Already-matching cells are
// useful context, but must not consume a slot and hide a later executable cell.
return !current.equals(desired)
&& (current.canBeReplaced() || settings.allowReplacement());
});
List<BuilderPreviewState.Cell> result = new ArrayList<>(traversal.nodes().size());
for (SurfacePlaneTraversal.Node node : traversal.nodes()) {
BlockState source = level.getBlockState(node.pos());
if (mode == BuilderMode.DEMOLISH) {
BuilderPreviewState.Kind kind = source.is(BuilderTags.CONSTRUCTION_PROTECTED)
|| source.is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.INVALID : BuilderPreviewState.Kind.DEMOLISH;
result.add(new BuilderPreviewState.Cell(node.pos(), source, kind, false));
} else {
BlockPos target = node.pos().relative(hit.getDirection());
BlockState desired = override == null ? source : override;
BlockState current = level.getBlockState(target);
BuilderPreviewState.Kind kind = current.equals(desired)
? BuilderPreviewState.Kind.BLUEPRINT
: BuilderReplacementPolicy.canReplaceTarget(current, level, target,
settings.allowReplacement(), creative) && desired.canSurvive(level, target)
&& !desired.is(BuilderTags.CONSTRUCTION_PROTECTED)
&& !desired.is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.BUILD : BuilderPreviewState.Kind.INVALID;
result.add(new BuilderPreviewState.Cell(target, desired, kind,
kind == BuilderPreviewState.Kind.BUILD));
}
}
return List.copyOf(result);
}
/** Checks a bounded slice of the previous result and only reruns BFS after observable drift. */
private static boolean hasPreviewDrift(ClientLevel level, PreviewKey key) {
if (lastCells.isEmpty() || lastFace == null) return false;
int checks = Math.min(VALIDATION_CELLS_PER_TICK, lastCells.size());
for (int count = 0; count < checks; count++) {
if (validationCursor >= lastCells.size()) validationCursor = 0;
BuilderPreviewState.Cell cell = lastCells.get(validationCursor++);
BlockState current = level.getBlockState(cell.pos());
if (key.mode() == BuilderMode.DEMOLISH) {
if (!SurfacePlaneTraversal.hasExposedReferenceFace(
level, cell.pos(), lastFace)) return true;
BuilderPreviewState.Kind currentKind = current.is(BuilderTags.CONSTRUCTION_PROTECTED)
|| current.is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.INVALID : BuilderPreviewState.Kind.DEMOLISH;
if (!current.equals(cell.state()) || currentKind != cell.kind()) return true;
continue;
}
BuilderPreviewState.Kind currentKind = current.equals(cell.state())
? BuilderPreviewState.Kind.BLUEPRINT
: BuilderReplacementPolicy.canReplaceTarget(current, level, cell.pos(),
key.allowReplacement(), key.creative())
&& cell.state().canSurvive(level, cell.pos())
&& !cell.state().is(BuilderTags.CONSTRUCTION_PROTECTED)
&& !cell.state().is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.BUILD : BuilderPreviewState.Kind.INVALID;
if (currentKind != cell.kind()) return true;
BlockState source = level.getBlockState(cell.pos().relative(lastFace.getOpposite()));
if (!isVisibleBuildReference(level, cell.pos().relative(lastFace.getOpposite()),
lastFace, source, key.mode(), key.allowReplacement())) return true;
if (lastOverride == null) {
if (!source.equals(cell.state())) return true;
} else if (!SurfacePlaneTraversal.isReference(source)
|| key.match() == SurfaceMatchMode.SAME_BLOCK
&& !BuilderBlockCompatibility.sameSurfaceType(key.seedState(), source)) {
return true;
}
}
return false;
}
/** Client mirror of SurfacePlanner's visible reference/replacement surface boundary. */
private static boolean isVisibleBuildReference(ClientLevel level, BlockPos referencePos,
Direction face, BlockState reference,
BuilderMode mode, boolean allowReplacement) {
if (SurfacePlaneTraversal.hasExposedReferenceFace(
level, referencePos, face, reference)) return true;
if (mode != BuilderMode.BUILD || !allowReplacement) return false;
BlockPos target = referencePos.relative(face);
BlockState obstacle = level.getBlockState(target);
return BuilderReplacementPolicy.requiresBreaking(obstacle)
&& SurfacePlaneTraversal.hasExposedReferenceFace(level, target, face, obstacle);
}
private record PreviewKey(ClientLevel level, BlockPos seed, Direction face, BlockState seedState,
BuilderMode mode, SurfaceMatchMode match, int buildLimit,
int demolishLimit, boolean allowReplacement, boolean creative,
BlockState override, BlockState targetSeedState) { }
}