package com.xfestudio.mydimension.client.builder;
import com.xfestudio.mydimension.MyDimension;
import com.xfestudio.mydimension.network.ModNetwork;
import com.xfestudio.mydimension.network.builder.BuilderClientPacketHooks;
import com.xfestudio.mydimension.network.builder.BuilderCommandPacket;
import com.xfestudio.mydimension.network.builder.BuilderPreviewPacket;
import com.xfestudio.mydimension.network.builder.BuilderSnapshotPacket;
import com.xfestudio.mydimension.config.BuilderAvailability;
import com.xfestudio.mydimension.builder.blueprint.BlueprintData;
import com.xfestudio.mydimension.builder.blueprint.BlueprintPlacementPlan;
import com.xfestudio.mydimension.builder.blueprint.BlueprintSaveMode;
import com.xfestudio.mydimension.builder.blueprint.BlueprintTransform;
import com.xfestudio.mydimension.builder.BuilderReplacementPolicy;
import com.xfestudio.mydimension.builder.BuilderTags;
import com.xfestudio.mydimension.builder.blueprint.client.ClientBlueprintTransfers;
import com.xfestudio.mydimension.client.builder.blueprint.BlueprintCaptureScreen;
import com.xfestudio.mydimension.client.builder.blueprint.ClientBlueprintLibrary;
import com.xfestudio.mydimension.network.blueprint.BlueprintCaptureRequestPacket;
import com.xfestudio.mydimension.network.blueprint.BlueprintPlaceRequestPacket;
import com.xfestudio.mydimension.network.blueprint.BlueprintSelectionStartPacket;
import com.xfestudio.mydimension.network.blueprint.BlueprintSelectionCancelPacket;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
/** Converts client UX intents to ordinary builder packets and applies S2C state. */
@Mod.EventBusSubscriber(modid = MyDimension.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD,
value = Dist.CLIENT)
public final class BuilderClientNetworkBridge implements BuilderClientBridge,
BuilderClientPacketHooks.Receiver {
private static final BuilderClientNetworkBridge INSTANCE = new BuilderClientNetworkBridge();
private static final ClientBlueprintTransfers BLUEPRINT_TRANSFERS = new ClientBlueprintTransfers();
private volatile BuilderClientSnapshot snapshot = BuilderClientSnapshot.EMPTY;
@Nullable private volatile UUID selectedBlueprintId;
@Nullable private BlueprintData selectedBlueprint;
@Nullable private UUID blueprintToken;
@Nullable private UUID pendingUpload;
@Nullable private UUID pendingCapture;
@Nullable private UUID pendingSelectionCapture;
@Nullable private UUID pendingPlacement;
@Nullable private BlueprintData cachedSelectionBlueprint;
@Nullable private UUID cachedSelectionToken;
private long cachedSelectionAtNanos;
private BlueprintTransform transform = BlueprintTransform.NONE;
private BlockPos offset = BlockPos.ZERO;
@Nullable private BlockPos lastPreviewAnchor;
private boolean previewDirty;
private int blueprintTargetMissTicks;
private BuilderClientNetworkBridge() {
}
@net.minecraftforge.eventbus.api.SubscribeEvent
public static void clientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> {
BuilderClientServices.install(INSTANCE);
BuilderClientPacketHooks.install(INSTANCE);
BLUEPRINT_TRANSFERS.install();
});
}
@Override
public BuilderClientSnapshot snapshot() {
BuilderClientSnapshot value = snapshot;
UUID selected = selectedBlueprintId;
if (java.util.Objects.equals(value.selectedBlueprintId(), selected)) return value;
return new BuilderClientSnapshot(value.enabled(), value.mode(), value.surfaceMatch(),
value.historyRecording(), value.allowReplacement(),
value.buildLimit(), value.demolishLimit(), value.maximumBuildLimit(),
value.maximumDemolishLimit(), value.reach(), value.status(), value.activeJobId(),
value.completedBlocks(), value.totalBlocks(), value.canUndo(), value.canRedo(),
value.anchors(), value.history(), selected);
}
@Override
public void requestSnapshot() {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.requestSnapshot());
}
@Override
public void send(BuilderClientCommand command) {
if (command instanceof BuilderClientCommand.OpenMenu) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.openMenu());
} else if (command instanceof BuilderClientCommand.SetMode value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setMode(value.mode()));
} else if (command instanceof BuilderClientCommand.SetSurfaceMatch value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setMatch(value.mode()));
} else if (command instanceof BuilderClientCommand.SetLimits value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setLimits(
value.buildLimit(), value.demolishLimit()));
} else if (command instanceof BuilderClientCommand.SetHistoryRecording value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setHistoryRecording(value.enabled()));
} else if (command instanceof BuilderClientCommand.SetAllowReplacement value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setAllowReplacement(value.enabled()));
} else if (command instanceof BuilderClientCommand.UseTarget value) {
switch (value.kind()) {
case AUTO -> ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.use(
target(value.target()), value.interactionOverride()));
case RESUME_MISSING -> ModNetwork.CHANNEL.sendToServer(
BuilderCommandPacket.resume(value.activeJobId()));
case PLACE_BLUEPRINT -> placeBlueprint(value.target());
}
} else if (command instanceof BuilderClientCommand.CancelActive value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.cancel(value.activeJobId()));
} else if (command instanceof BuilderClientCommand.CancelBlueprint) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.cancelBlueprint());
} else if (command instanceof BuilderClientCommand.Undo) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.undo());
} else if (command instanceof BuilderClientCommand.Redo) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.redo());
} else if (command instanceof BuilderClientCommand.UnbindAnchor value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.unbindAnchor(value.anchorId()));
} else if (command instanceof BuilderClientCommand.MoveAnchor value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.moveAnchor(value.anchorId(), value.delta()));
} else if (command instanceof BuilderClientCommand.SetAnchorPublic value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setAnchorPublic(
value.anchorId(), value.publicAccess()));
} else if (command instanceof BuilderClientCommand.SetAnchorPlayer value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setAnchorPlayer(
value.anchorId(), value.playerName(), value.authorize()));
} else if (command instanceof BuilderClientCommand.SelectBlueprint value) {
selectedBlueprintId = value.blueprintId();
selectBlueprint(value.blueprintId());
} else if (command instanceof BuilderClientCommand.SelectBlueprintPoint value) {
selectPoint(value.target());
} else if (command instanceof BuilderClientCommand.ExecuteBlueprintAction value) {
executeBlueprintAction(value.action());
} else if (command instanceof BuilderClientCommand.AdjustBlueprint value) {
adjustBlueprint(value.action(), value.direction());
} else if (command instanceof BuilderClientCommand.ConfirmBlueprintAdjustment) {
previewDirty = true;
}
}
@Override
public void snapshot(BuilderSnapshotPacket packet) {
boolean replacementSettingChanged = snapshot.allowReplacement() != packet.allowReplacement();
availability(packet.enabled());
List<BuilderClientSnapshot.AnchorView> anchors = packet.anchors().stream()
.map(anchor -> new BuilderClientSnapshot.AnchorView(anchor.id(), anchor.name(),
anchor.dimension(), anchor.packedPos(),
BuilderClientSnapshot.AnchorStatus.valueOf(anchor.status().name()),
anchor.owner(), anchor.publicAccess()))
.toList();
List<BuilderClientSnapshot.HistoryView> history = packet.history().stream()
.map(entry -> new BuilderClientSnapshot.HistoryView(entry.id(), entry.label(),
entry.dimension(), entry.changedBlocks(), entry.createdAt(),
BuilderClientSnapshot.HistoryStatus.valueOf(entry.status().name())))
.toList();
snapshot = new BuilderClientSnapshot(packet.enabled(), packet.mode(), packet.surfaceMatch(),
packet.historyRecording(), packet.allowReplacement(),
packet.buildLimit(), packet.demolishLimit(), packet.maximumBuildLimit(),
packet.maximumDemolishLimit(), packet.reach(), packet.status(), packet.activeJobId(),
packet.completedBlocks(), packet.totalBlocks(), packet.canUndo(), packet.canRedo(),
anchors, history, selectedBlueprintId);
if (replacementSettingChanged) previewDirty = true;
}
@Override
public void availability(boolean enabled) {
// Every ordinary build result carries the same enabled bit. Rebuilding all creative tabs
// for that unchanged bit caused a large client-frame hitch after every click.
if (!BuilderAvailability.acceptServerValue(enabled)) return;
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player == null || minecraft.level == null) return;
boolean operatorTab = minecraft.player.canUseGameMasterBlocks()
&& minecraft.options.operatorItemsTab().get();
// Force the vanilla creative-tab cache through a distinct key. If the creative screen is
// open, leave the opposite key cached so its next container tick performs the visible refresh.
net.minecraft.world.item.CreativeModeTabs.tryRebuildTabContents(
minecraft.player.connection.enabledFeatures(), !operatorTab,
minecraft.player.level().registryAccess());
if (!(minecraft.screen instanceof net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen)) {
net.minecraft.world.item.CreativeModeTabs.tryRebuildTabContents(
minecraft.player.connection.enabledFeatures(), operatorTab,
minecraft.player.level().registryAccess());
}
}
@Override
public void preview(BuilderPreviewPacket packet) {
ResourceKey<Level> dimension = ResourceKey.create(Registries.DIMENSION, packet.dimension());
List<BuilderPreviewState.Cell> cells = packet.cells().stream()
.map(cell -> new BuilderPreviewState.Cell(cell.pos(), cell.state(),
BuilderPreviewState.Kind.valueOf(cell.kind().name()), cell.ghost()))
.toList();
BuilderPreviewState state = BuilderPreviewState.get();
BuilderPreviewState.Snapshot current = state.snapshot();
UUID currentJobId = current == null ? null : current.activeJobId();
// Pending blueprint work is moved from SavedData into the active worker before this
// preview packet is built. The paired lightweight snapshot still carries its transaction
// id, so that brief empty packet is not a real completion and must not erase yellow cells.
boolean preserveTransientEmpty = cells.isEmpty() && packet.activeJobId() == null
&& currentJobId != null && currentJobId.equals(snapshot.activeJobId());
state.updateMissingPreview(dimension, cells, packet.activeJobId(), packet.cancelable(),
packet.revision(), preserveTransientEmpty);
}
@Override
public void openMenu() {
Minecraft minecraft = Minecraft.getInstance();
if (!(minecraft.screen instanceof BuilderToolScreen)) {
BuilderClientServices.openToolScreen();
}
}
private static BuilderCommandPacket.Target target(BuilderClientCommand.Target value) {
return new BuilderCommandPacket.Target(value.blockPos(), value.face(), value.inside());
}
private static void selectPoint(BuilderClientCommand.Target target) {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.level == null) return;
ResourceKey<Level> dimension = minecraft.level.dimension();
BuilderPreviewState.Snapshot current = BuilderPreviewState.get().snapshot();
BuilderPreviewState.Selection selection;
if (current != null && current.dimension().equals(dimension)
&& current.selection().first() != null && current.selection().second() == null) {
selection = new BuilderPreviewState.Selection(dimension,
current.selection().first(), target.blockPos());
} else {
INSTANCE.clearCachedSelectionCopy();
INSTANCE.pendingSelectionCapture = null;
selection = new BuilderPreviewState.Selection(dimension, target.blockPos(), null);
ModNetwork.CHANNEL.sendToServer(new BlueprintSelectionStartPacket(target.blockPos()));
}
boolean complete = selection.first() != null && selection.second() != null;
// Completing a source cuboid does not itself create a movable deployment. That only becomes
// active after the validated capture download is available and refreshBlueprintPreview()
// has built its destination cells.
BuilderPreviewState.get().updateSelection(selection);
if (complete) {
requestSelectionCopy();
}
}
/**
* Rebuilds a movable deployment preview from the persistent, server-validated source cuboid.
* This is also used by the wheel after a player cancels only the copied preview: the blue
* source selection remains intact and can therefore be copied again without reselecting it.
*/
public static boolean requestSelectionCopy() {
BuilderPreviewState.Snapshot preview = BuilderPreviewState.get().snapshot();
if (preview == null || preview.selection() == null || !preview.selection().complete()
|| INSTANCE.pendingSelectionCapture != null) return false;
// A source-selection capture supersedes any library upload that may still be in flight.
// Otherwise that late upload could pair this selection's BlueprintData with an unrelated
// cache token and make the visible preview differ from the eventual placement.
INSTANCE.selectedBlueprintId = null;
INSTANCE.pendingUpload = null;
if (INSTANCE.restoreFreshSelectionCopy()) return true;
UUID request = UUID.randomUUID();
INSTANCE.pendingSelectionCapture = request;
ModNetwork.CHANNEL.sendToServer(new BlueprintCaptureRequestPacket(request,
preview.selection().first(), preview.selection().second(), BlueprintSaveMode.BLOCKS_ONLY,
"Unsaved Selection", false));
return true;
}
public static boolean requestCapture(String name, BlueprintSaveMode mode) {
BuilderPreviewState.Snapshot preview = BuilderPreviewState.get().snapshot();
if (preview == null || preview.selection().first() == null
|| preview.selection().second() == null || INSTANCE.pendingCapture != null) return false;
UUID request = UUID.randomUUID();
INSTANCE.pendingCapture = request;
ModNetwork.CHANNEL.sendToServer(new BlueprintCaptureRequestPacket(request,
preview.selection().first(), preview.selection().second(), mode, name, true));
return true;
}
/** Stops routing a late capture response into a dialog the player already closed. */
public static void cancelPendingCapture() {
INSTANCE.pendingCapture = null;
}
public static void clientTick() {
BLUEPRINT_TRANSFERS.tick();
INSTANCE.pollBlueprintTransfers();
boolean force = INSTANCE.previewDirty;
INSTANCE.previewDirty = false;
INSTANCE.refreshBlueprintPreview(force);
}
static void requestBlueprintPreviewRefresh() {
// Coalesce high-resolution wheel input into one rebuild per client tick.
INSTANCE.previewDirty = true;
}
public static void clearBlueprintSession() {
INSTANCE.selectedBlueprint = null;
INSTANCE.blueprintToken = null;
INSTANCE.pendingUpload = null;
INSTANCE.pendingCapture = null;
INSTANCE.pendingSelectionCapture = null;
INSTANCE.pendingPlacement = null;
INSTANCE.clearCachedSelectionCopy();
INSTANCE.lastPreviewAnchor = null;
INSTANCE.previewDirty = false;
INSTANCE.blueprintTargetMissTicks = 0;
INSTANCE.transform = BlueprintTransform.NONE;
INSTANCE.offset = BlockPos.ZERO;
BLUEPRINT_TRANSFERS.clear();
}
public static void cancelBlueprintWorkflow() {
ModNetwork.CHANNEL.sendToServer(new BlueprintSelectionCancelPacket());
clearBlueprintSession();
BuilderPreviewState.get().clearLocalWorkflow();
}
/** Drops only the transformed placement and keeps a source selection available for saving. */
public static void cancelPlacementPreview() {
INSTANCE.selectedBlueprintId = null;
INSTANCE.selectedBlueprint = null;
INSTANCE.blueprintToken = null;
INSTANCE.pendingUpload = null;
INSTANCE.pendingPlacement = null;
INSTANCE.lastPreviewAnchor = null;
INSTANCE.previewDirty = false;
INSTANCE.blueprintTargetMissTicks = 0;
INSTANCE.transform = BlueprintTransform.NONE;
INSTANCE.offset = BlockPos.ZERO;
BuilderPreviewState.get().clearPlacementPreview();
}
/** Cancels only the source-corner workflow; an already copied deployment remains selected. */
public static void cancelSourceSelection() {
ModNetwork.CHANNEL.sendToServer(new BlueprintSelectionCancelPacket());
INSTANCE.pendingSelectionCapture = null;
INSTANCE.clearCachedSelectionCopy();
BuilderPreviewState.get().clearSelection();
}
private void selectBlueprint(UUID id) {
ClientBlueprintLibrary.get().blueprint(id).ifPresentOrElse(blueprint -> {
selectedBlueprint = blueprint;
blueprintToken = null;
transform = BlueprintTransform.NONE;
offset = BlockPos.ZERO;
lastPreviewAnchor = null;
try {
pendingUpload = BLUEPRINT_TRANSFERS.upload(blueprint);
} catch (java.io.IOException ignored) {
pendingUpload = null;
}
}, () -> {
selectedBlueprint = null;
blueprintToken = null;
});
}
private void pollBlueprintTransfers() {
if (pendingUpload != null) {
BLUEPRINT_TRANSFERS.takeResult(pendingUpload).ifPresent(result -> {
if (result.success()) blueprintToken = result.cacheToken();
else showBlueprintMessage(result.message());
pendingUpload = null;
lastPreviewAnchor = null;
});
}
if (pendingCapture != null) {
UUID captureId = pendingCapture;
java.util.Optional<ClientBlueprintTransfers.Download> download =
BLUEPRINT_TRANSFERS.takeDownload(captureId);
if (download.isPresent()) {
pendingCapture = null;
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.screen instanceof BlueprintCaptureScreen captureScreen) {
captureScreen.acceptCapture(download.get().blueprint());
}
} else BLUEPRINT_TRANSFERS.takeResult(captureId).ifPresent(result -> {
if (!result.success()) {
pendingCapture = null;
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.screen instanceof BlueprintCaptureScreen captureScreen) {
captureScreen.captureFailed(result.message());
} else {
showBlueprintMessage(result.message());
}
}
});
}
if (pendingSelectionCapture != null) {
UUID selectionId = pendingSelectionCapture;
java.util.Optional<ClientBlueprintTransfers.Download> download =
BLUEPRINT_TRANSFERS.takeDownload(selectionId);
if (download.isPresent()) {
pendingSelectionCapture = null;
cachedSelectionBlueprint = download.get().blueprint();
cachedSelectionToken = download.get().cacheToken();
cachedSelectionAtNanos = System.nanoTime();
selectedBlueprint = cachedSelectionBlueprint;
blueprintToken = cachedSelectionToken;
transform = BlueprintTransform.NONE;
offset = BlockPos.ZERO;
lastPreviewAnchor = null;
} else BLUEPRINT_TRANSFERS.takeResult(selectionId).ifPresent(result -> {
if (!result.success()) {
pendingSelectionCapture = null;
showBlueprintMessage(result.message());
}
});
}
if (pendingPlacement != null) {
UUID placementId = pendingPlacement;
BLUEPRINT_TRANSFERS.takeResult(placementId).ifPresent(result -> {
pendingPlacement = null;
showBlueprintMessage(result.message());
});
}
}
/**
* Covers the brief interval in which the server's anti-spam capture cooldown is still active.
* A just-cancelled deployment can be restored locally from the capture that produced it; after
* two seconds we recapture normally so later source-world edits and cache eviction are respected.
*/
private boolean restoreFreshSelectionCopy() {
long age = System.nanoTime() - cachedSelectionAtNanos;
if (cachedSelectionBlueprint == null || cachedSelectionToken == null
|| cachedSelectionAtNanos == 0L || age < 0L || age > 2_000_000_000L) {
clearCachedSelectionCopy();
return false;
}
selectedBlueprint = cachedSelectionBlueprint;
blueprintToken = cachedSelectionToken;
transform = BlueprintTransform.NONE;
offset = BlockPos.ZERO;
lastPreviewAnchor = null;
blueprintTargetMissTicks = 0;
previewDirty = true;
return true;
}
private void clearCachedSelectionCopy() {
cachedSelectionBlueprint = null;
cachedSelectionToken = null;
cachedSelectionAtNanos = 0L;
}
private static void showBlueprintMessage(String message) {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player != null && message != null && !message.isBlank()) {
minecraft.player.displayClientMessage(Component.literal(message), false);
}
}
private void executeBlueprintAction(BlueprintAltActionController.Action action) {
if (action == BlueprintAltActionController.Action.COPY_SELECTION) {
requestSelectionCopy();
return;
}
if (action == BlueprintAltActionController.Action.SAVE) {
BuilderPreviewState.Snapshot preview = BuilderPreviewState.get().snapshot();
if (preview != null && preview.selection() != null && preview.selection().complete()) {
Minecraft minecraft = Minecraft.getInstance();
minecraft.setScreen(new BlueprintCaptureScreen(minecraft.screen));
}
return;
}
if (selectedBlueprint == null) return;
transform = switch (action) {
case FLIP_X -> new BlueprintTransform(!transform.flipX(), transform.flipY(), transform.flipZ(),
transform.rotationY());
case FLIP_Y -> new BlueprintTransform(transform.flipX(), !transform.flipY(), transform.flipZ(),
transform.rotationY());
case FLIP_Z -> new BlueprintTransform(transform.flipX(), transform.flipY(), !transform.flipZ(),
transform.rotationY());
case RESET -> BlueprintTransform.NONE;
default -> transform;
};
if (action == BlueprintAltActionController.Action.RESET) offset = BlockPos.ZERO;
previewDirty = true;
}
private void adjustBlueprint(BlueprintAltActionController.Action action, int direction) {
if (selectedBlueprint == null || direction == 0) return;
switch (action) {
case ROTATE_Y -> transform = new BlueprintTransform(transform.flipX(), transform.flipY(),
transform.flipZ(), rotate(transform.rotationY(), direction));
case OFFSET_X -> offset = offset.offset(direction, 0, 0);
case OFFSET_Y -> offset = offset.offset(0, direction, 0);
case OFFSET_Z -> offset = offset.offset(0, 0, direction);
default -> { return; }
}
previewDirty = true;
}
private static Rotation rotate(Rotation current, int direction) {
Rotation[] clockwise = { Rotation.NONE, Rotation.CLOCKWISE_90, Rotation.CLOCKWISE_180,
Rotation.COUNTERCLOCKWISE_90 };
int index = 0;
for (int i = 0; i < clockwise.length; i++) if (clockwise[i] == current) index = i;
return clockwise[Math.floorMod(index + direction, clockwise.length)];
}
private void refreshBlueprintPreview(boolean force) {
if (selectedBlueprint == null || blueprintToken == null) return;
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player == null || minecraft.level == null || minecraft.screen != null
|| !BuilderClientServices.isHoldingRealmwright(minecraft)) return;
BuilderClientCommand.Target airTarget = BuilderClientEvents.airPlacementTarget();
BlockPos anchor;
if (airTarget != null && airTarget.direct()) {
anchor = airTarget.blockPos().offset(offset);
} else {
HitResult picked = minecraft.player.pick(Math.max(1, snapshot().reach()), 1.0F, false);
if (!(picked instanceof BlockHitResult hit) || picked.getType() != HitResult.Type.BLOCK) {
if (lastPreviewAnchor != null) {
if (++blueprintTargetMissTicks >= 2) {
lastPreviewAnchor = null;
blueprintTargetMissTicks = 0;
clearBlueprintCells(minecraft);
}
}
return;
}
anchor = hit.getBlockPos().relative(hit.getDirection()).offset(offset);
}
blueprintTargetMissTicks = 0;
boolean currentGenerationIntact = BuilderPreviewState.get()
.hasCompleteDeploymentGeneration(selectedBlueprint.blocks().size());
if (!force && anchor.equals(lastPreviewAnchor) && currentGenerationIntact) return;
lastPreviewAnchor = anchor;
BlueprintPlacementPlan plan = BlueprintPlacementPlan.create(selectedBlueprint, transform, anchor);
List<BuilderPreviewState.Cell> cells = plan.blocks().stream().map(block -> {
net.minecraft.world.level.block.state.BlockState current = minecraft.level.getBlockState(block.worldPos());
BuilderPreviewState.Kind kind = current.equals(block.state())
? BuilderPreviewState.Kind.BLUEPRINT
: BuilderReplacementPolicy.canReplaceTarget(current, minecraft.level, block.worldPos(),
snapshot().allowReplacement(), minecraft.player.isCreative())
&& block.state().canSurvive(minecraft.level, block.worldPos())
&& !block.state().is(BuilderTags.CONSTRUCTION_PROTECTED)
&& !block.state().is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.BUILD : BuilderPreviewState.Kind.INVALID;
// An already-matching BLUEPRINT cell is the real world block itself. Baking a
// second translucent model at the exact same coordinates causes depth fighting
// while the player moves; retain its blue outline only.
return new BuilderPreviewState.Cell(block.worldPos(), block.state(), kind,
kind == BuilderPreviewState.Kind.BUILD);
}).toList();
BuilderPreviewState.get().updateDeploymentPreview(minecraft.level.dimension(), cells);
}
private static void clearBlueprintCells(Minecraft minecraft) {
BuilderPreviewState.get().updateDeploymentPreview(minecraft.level.dimension(), List.of());
}
private void placeBlueprint(BuilderClientCommand.Target target) {
if (blueprintToken == null || selectedBlueprint == null) return;
BlockPos anchor = (target.direct() ? target.blockPos()
: target.blockPos().relative(target.face())).offset(offset);
UUID requestId = UUID.randomUUID();
pendingPlacement = requestId;
ModNetwork.CHANNEL.sendToServer(new BlueprintPlaceRequestPacket(requestId, blueprintToken,
anchor, transform));
}
}
package com.xfestudio.mydimension.client.builder;
import com.xfestudio.mydimension.MyDimension;
import com.xfestudio.mydimension.network.ModNetwork;
import com.xfestudio.mydimension.network.builder.BuilderClientPacketHooks;
import com.xfestudio.mydimension.network.builder.BuilderCommandPacket;
import com.xfestudio.mydimension.network.builder.BuilderPreviewPacket;
import com.xfestudio.mydimension.network.builder.BuilderSnapshotPacket;
import com.xfestudio.mydimension.config.BuilderAvailability;
import com.xfestudio.mydimension.builder.blueprint.BlueprintData;
import com.xfestudio.mydimension.builder.blueprint.BlueprintPlacementPlan;
import com.xfestudio.mydimension.builder.blueprint.BlueprintSaveMode;
import com.xfestudio.mydimension.builder.blueprint.BlueprintTransform;
import com.xfestudio.mydimension.builder.BuilderReplacementPolicy;
import com.xfestudio.mydimension.builder.BuilderTags;
import com.xfestudio.mydimension.builder.blueprint.client.ClientBlueprintTransfers;
import com.xfestudio.mydimension.client.builder.blueprint.BlueprintCaptureScreen;
import com.xfestudio.mydimension.client.builder.blueprint.ClientBlueprintLibrary;
import com.xfestudio.mydimension.network.blueprint.BlueprintCaptureRequestPacket;
import com.xfestudio.mydimension.network.blueprint.BlueprintPlaceRequestPacket;
import com.xfestudio.mydimension.network.blueprint.BlueprintSelectionStartPacket;
import com.xfestudio.mydimension.network.blueprint.BlueprintSelectionCancelPacket;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
/** Converts client UX intents to ordinary builder packets and applies S2C state. */
@Mod.EventBusSubscriber(modid = MyDimension.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD,
value = Dist.CLIENT)
public final class BuilderClientNetworkBridge implements BuilderClientBridge,
BuilderClientPacketHooks.Receiver {
private static final BuilderClientNetworkBridge INSTANCE = new BuilderClientNetworkBridge();
private static final ClientBlueprintTransfers BLUEPRINT_TRANSFERS = new ClientBlueprintTransfers();
private volatile BuilderClientSnapshot snapshot = BuilderClientSnapshot.EMPTY;
@Nullable private volatile UUID selectedBlueprintId;
@Nullable private BlueprintData selectedBlueprint;
@Nullable private UUID blueprintToken;
@Nullable private UUID pendingUpload;
@Nullable private UUID pendingCapture;
@Nullable private UUID pendingSelectionCapture;
@Nullable private UUID pendingPlacement;
@Nullable private BlueprintData cachedSelectionBlueprint;
@Nullable private UUID cachedSelectionToken;
private long cachedSelectionAtNanos;
private BlueprintTransform transform = BlueprintTransform.NONE;
private BlockPos offset = BlockPos.ZERO;
@Nullable private BlockPos lastPreviewAnchor;
private boolean previewDirty;
private int blueprintTargetMissTicks;
private BuilderClientNetworkBridge() {
}
@net.minecraftforge.eventbus.api.SubscribeEvent
public static void clientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> {
BuilderClientServices.install(INSTANCE);
BuilderClientPacketHooks.install(INSTANCE);
BLUEPRINT_TRANSFERS.install();
});
}
@Override
public BuilderClientSnapshot snapshot() {
BuilderClientSnapshot value = snapshot;
UUID selected = selectedBlueprintId;
if (java.util.Objects.equals(value.selectedBlueprintId(), selected)) return value;
return new BuilderClientSnapshot(value.enabled(), value.mode(), value.surfaceMatch(),
value.historyRecording(), value.allowReplacement(),
value.buildLimit(), value.demolishLimit(), value.maximumBuildLimit(),
value.maximumDemolishLimit(), value.reach(), value.status(), value.activeJobId(),
value.completedBlocks(), value.totalBlocks(), value.canUndo(), value.canRedo(),
value.anchors(), value.history(), selected);
}
@Override
public void requestSnapshot() {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.requestSnapshot());
}
@Override
public void send(BuilderClientCommand command) {
if (command instanceof BuilderClientCommand.OpenMenu) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.openMenu());
} else if (command instanceof BuilderClientCommand.SetMode value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setMode(value.mode()));
} else if (command instanceof BuilderClientCommand.SetSurfaceMatch value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setMatch(value.mode()));
} else if (command instanceof BuilderClientCommand.SetLimits value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setLimits(
value.buildLimit(), value.demolishLimit()));
} else if (command instanceof BuilderClientCommand.SetHistoryRecording value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setHistoryRecording(value.enabled()));
} else if (command instanceof BuilderClientCommand.SetAllowReplacement value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setAllowReplacement(value.enabled()));
} else if (command instanceof BuilderClientCommand.UseTarget value) {
switch (value.kind()) {
case AUTO -> ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.use(
target(value.target()), value.interactionOverride()));
case RESUME_MISSING -> ModNetwork.CHANNEL.sendToServer(
BuilderCommandPacket.resume(value.activeJobId()));
case PLACE_BLUEPRINT -> placeBlueprint(value.target());
}
} else if (command instanceof BuilderClientCommand.CancelActive value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.cancel(value.activeJobId()));
} else if (command instanceof BuilderClientCommand.CancelBlueprint) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.cancelBlueprint());
} else if (command instanceof BuilderClientCommand.Undo) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.undo());
} else if (command instanceof BuilderClientCommand.Redo) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.redo());
} else if (command instanceof BuilderClientCommand.UnbindAnchor value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.unbindAnchor(value.anchorId()));
} else if (command instanceof BuilderClientCommand.MoveAnchor value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.moveAnchor(value.anchorId(), value.delta()));
} else if (command instanceof BuilderClientCommand.SetAnchorPublic value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setAnchorPublic(
value.anchorId(), value.publicAccess()));
} else if (command instanceof BuilderClientCommand.SetAnchorPlayer value) {
ModNetwork.CHANNEL.sendToServer(BuilderCommandPacket.setAnchorPlayer(
value.anchorId(), value.playerName(), value.authorize()));
} else if (command instanceof BuilderClientCommand.SelectBlueprint value) {
selectedBlueprintId = value.blueprintId();
selectBlueprint(value.blueprintId());
} else if (command instanceof BuilderClientCommand.SelectBlueprintPoint value) {
selectPoint(value.target());
} else if (command instanceof BuilderClientCommand.ExecuteBlueprintAction value) {
executeBlueprintAction(value.action());
} else if (command instanceof BuilderClientCommand.AdjustBlueprint value) {
adjustBlueprint(value.action(), value.direction());
} else if (command instanceof BuilderClientCommand.ConfirmBlueprintAdjustment) {
previewDirty = true;
}
}
@Override
public void snapshot(BuilderSnapshotPacket packet) {
boolean replacementSettingChanged = snapshot.allowReplacement() != packet.allowReplacement();
availability(packet.enabled());
List<BuilderClientSnapshot.AnchorView> anchors = packet.anchors().stream()
.map(anchor -> new BuilderClientSnapshot.AnchorView(anchor.id(), anchor.name(),
anchor.dimension(), anchor.packedPos(),
BuilderClientSnapshot.AnchorStatus.valueOf(anchor.status().name()),
anchor.owner(), anchor.publicAccess()))
.toList();
List<BuilderClientSnapshot.HistoryView> history = packet.history().stream()
.map(entry -> new BuilderClientSnapshot.HistoryView(entry.id(), entry.label(),
entry.dimension(), entry.changedBlocks(), entry.createdAt(),
BuilderClientSnapshot.HistoryStatus.valueOf(entry.status().name())))
.toList();
snapshot = new BuilderClientSnapshot(packet.enabled(), packet.mode(), packet.surfaceMatch(),
packet.historyRecording(), packet.allowReplacement(),
packet.buildLimit(), packet.demolishLimit(), packet.maximumBuildLimit(),
packet.maximumDemolishLimit(), packet.reach(), packet.status(), packet.activeJobId(),
packet.completedBlocks(), packet.totalBlocks(), packet.canUndo(), packet.canRedo(),
anchors, history, selectedBlueprintId);
if (replacementSettingChanged) previewDirty = true;
}
@Override
public void availability(boolean enabled) {
// Every ordinary build result carries the same enabled bit. Rebuilding all creative tabs
// for that unchanged bit caused a large client-frame hitch after every click.
if (!BuilderAvailability.acceptServerValue(enabled)) return;
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player == null || minecraft.level == null) return;
boolean operatorTab = minecraft.player.canUseGameMasterBlocks()
&& minecraft.options.operatorItemsTab().get();
// Force the vanilla creative-tab cache through a distinct key. If the creative screen is
// open, leave the opposite key cached so its next container tick performs the visible refresh.
net.minecraft.world.item.CreativeModeTabs.tryRebuildTabContents(
minecraft.player.connection.enabledFeatures(), !operatorTab,
minecraft.player.level().registryAccess());
if (!(minecraft.screen instanceof net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen)) {
net.minecraft.world.item.CreativeModeTabs.tryRebuildTabContents(
minecraft.player.connection.enabledFeatures(), operatorTab,
minecraft.player.level().registryAccess());
}
}
@Override
public void preview(BuilderPreviewPacket packet) {
ResourceKey<Level> dimension = ResourceKey.create(Registries.DIMENSION, packet.dimension());
List<BuilderPreviewState.Cell> cells = packet.cells().stream()
.map(cell -> new BuilderPreviewState.Cell(cell.pos(), cell.state(),
BuilderPreviewState.Kind.valueOf(cell.kind().name()), cell.ghost()))
.toList();
BuilderPreviewState state = BuilderPreviewState.get();
BuilderPreviewState.Snapshot current = state.snapshot();
UUID currentJobId = current == null ? null : current.activeJobId();
// Pending blueprint work is moved from SavedData into the active worker before this
// preview packet is built. The paired lightweight snapshot still carries its transaction
// id, so that brief empty packet is not a real completion and must not erase yellow cells.
boolean preserveTransientEmpty = cells.isEmpty() && packet.activeJobId() == null
&& currentJobId != null && currentJobId.equals(snapshot.activeJobId());
state.updateMissingPreview(dimension, cells, packet.activeJobId(), packet.cancelable(),
packet.revision(), preserveTransientEmpty);
}
@Override
public void openMenu() {
Minecraft minecraft = Minecraft.getInstance();
if (!(minecraft.screen instanceof BuilderToolScreen)) {
BuilderClientServices.openToolScreen();
}
}
private static BuilderCommandPacket.Target target(BuilderClientCommand.Target value) {
return new BuilderCommandPacket.Target(value.blockPos(), value.face(), value.inside());
}
private static void selectPoint(BuilderClientCommand.Target target) {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.level == null) return;
ResourceKey<Level> dimension = minecraft.level.dimension();
BuilderPreviewState.Snapshot current = BuilderPreviewState.get().snapshot();
BuilderPreviewState.Selection selection;
if (current != null && current.dimension().equals(dimension)
&& current.selection().first() != null && current.selection().second() == null) {
selection = new BuilderPreviewState.Selection(dimension,
current.selection().first(), target.blockPos());
} else {
INSTANCE.clearCachedSelectionCopy();
INSTANCE.pendingSelectionCapture = null;
selection = new BuilderPreviewState.Selection(dimension, target.blockPos(), null);
ModNetwork.CHANNEL.sendToServer(new BlueprintSelectionStartPacket(target.blockPos()));
}
boolean complete = selection.first() != null && selection.second() != null;
// Completing a source cuboid does not itself create a movable deployment. That only becomes
// active after the validated capture download is available and refreshBlueprintPreview()
// has built its destination cells.
BuilderPreviewState.get().updateSelection(selection);
if (complete) {
requestSelectionCopy();
}
}
/**
* Rebuilds a movable deployment preview from the persistent, server-validated source cuboid.
* This is also used by the wheel after a player cancels only the copied preview: the blue
* source selection remains intact and can therefore be copied again without reselecting it.
*/
public static boolean requestSelectionCopy() {
BuilderPreviewState.Snapshot preview = BuilderPreviewState.get().snapshot();
if (preview == null || preview.selection() == null || !preview.selection().complete()
|| INSTANCE.pendingSelectionCapture != null) return false;
// A source-selection capture supersedes any library upload that may still be in flight.
// Otherwise that late upload could pair this selection's BlueprintData with an unrelated
// cache token and make the visible preview differ from the eventual placement.
INSTANCE.selectedBlueprintId = null;
INSTANCE.pendingUpload = null;
if (INSTANCE.restoreFreshSelectionCopy()) return true;
UUID request = UUID.randomUUID();
INSTANCE.pendingSelectionCapture = request;
ModNetwork.CHANNEL.sendToServer(new BlueprintCaptureRequestPacket(request,
preview.selection().first(), preview.selection().second(), BlueprintSaveMode.BLOCKS_ONLY,
"Unsaved Selection", false));
return true;
}
public static boolean requestCapture(String name, BlueprintSaveMode mode) {
BuilderPreviewState.Snapshot preview = BuilderPreviewState.get().snapshot();
if (preview == null || preview.selection().first() == null
|| preview.selection().second() == null || INSTANCE.pendingCapture != null) return false;
UUID request = UUID.randomUUID();
INSTANCE.pendingCapture = request;
ModNetwork.CHANNEL.sendToServer(new BlueprintCaptureRequestPacket(request,
preview.selection().first(), preview.selection().second(), mode, name, true));
return true;
}
/** Stops routing a late capture response into a dialog the player already closed. */
public static void cancelPendingCapture() {
INSTANCE.pendingCapture = null;
}
public static void clientTick() {
BLUEPRINT_TRANSFERS.tick();
INSTANCE.pollBlueprintTransfers();
boolean force = INSTANCE.previewDirty;
INSTANCE.previewDirty = false;
INSTANCE.refreshBlueprintPreview(force);
}
static void requestBlueprintPreviewRefresh() {
// Coalesce high-resolution wheel input into one rebuild per client tick.
INSTANCE.previewDirty = true;
}
public static void clearBlueprintSession() {
INSTANCE.selectedBlueprint = null;
INSTANCE.blueprintToken = null;
INSTANCE.pendingUpload = null;
INSTANCE.pendingCapture = null;
INSTANCE.pendingSelectionCapture = null;
INSTANCE.pendingPlacement = null;
INSTANCE.clearCachedSelectionCopy();
INSTANCE.lastPreviewAnchor = null;
INSTANCE.previewDirty = false;
INSTANCE.blueprintTargetMissTicks = 0;
INSTANCE.transform = BlueprintTransform.NONE;
INSTANCE.offset = BlockPos.ZERO;
BLUEPRINT_TRANSFERS.clear();
}
public static void cancelBlueprintWorkflow() {
ModNetwork.CHANNEL.sendToServer(new BlueprintSelectionCancelPacket());
clearBlueprintSession();
BuilderPreviewState.get().clearLocalWorkflow();
}
/** Drops only the transformed placement and keeps a source selection available for saving. */
public static void cancelPlacementPreview() {
INSTANCE.selectedBlueprintId = null;
INSTANCE.selectedBlueprint = null;
INSTANCE.blueprintToken = null;
INSTANCE.pendingUpload = null;
INSTANCE.pendingPlacement = null;
INSTANCE.lastPreviewAnchor = null;
INSTANCE.previewDirty = false;
INSTANCE.blueprintTargetMissTicks = 0;
INSTANCE.transform = BlueprintTransform.NONE;
INSTANCE.offset = BlockPos.ZERO;
BuilderPreviewState.get().clearPlacementPreview();
}
/** Cancels only the source-corner workflow; an already copied deployment remains selected. */
public static void cancelSourceSelection() {
ModNetwork.CHANNEL.sendToServer(new BlueprintSelectionCancelPacket());
INSTANCE.pendingSelectionCapture = null;
INSTANCE.clearCachedSelectionCopy();
BuilderPreviewState.get().clearSelection();
}
private void selectBlueprint(UUID id) {
ClientBlueprintLibrary.get().blueprint(id).ifPresentOrElse(blueprint -> {
selectedBlueprint = blueprint;
blueprintToken = null;
transform = BlueprintTransform.NONE;
offset = BlockPos.ZERO;
lastPreviewAnchor = null;
try {
pendingUpload = BLUEPRINT_TRANSFERS.upload(blueprint);
} catch (java.io.IOException ignored) {
pendingUpload = null;
}
}, () -> {
selectedBlueprint = null;
blueprintToken = null;
});
}
private void pollBlueprintTransfers() {
if (pendingUpload != null) {
BLUEPRINT_TRANSFERS.takeResult(pendingUpload).ifPresent(result -> {
if (result.success()) blueprintToken = result.cacheToken();
else showBlueprintMessage(result.message());
pendingUpload = null;
lastPreviewAnchor = null;
});
}
if (pendingCapture != null) {
UUID captureId = pendingCapture;
java.util.Optional<ClientBlueprintTransfers.Download> download =
BLUEPRINT_TRANSFERS.takeDownload(captureId);
if (download.isPresent()) {
pendingCapture = null;
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.screen instanceof BlueprintCaptureScreen captureScreen) {
captureScreen.acceptCapture(download.get().blueprint());
}
} else BLUEPRINT_TRANSFERS.takeResult(captureId).ifPresent(result -> {
if (!result.success()) {
pendingCapture = null;
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.screen instanceof BlueprintCaptureScreen captureScreen) {
captureScreen.captureFailed(result.message());
} else {
showBlueprintMessage(result.message());
}
}
});
}
if (pendingSelectionCapture != null) {
UUID selectionId = pendingSelectionCapture;
java.util.Optional<ClientBlueprintTransfers.Download> download =
BLUEPRINT_TRANSFERS.takeDownload(selectionId);
if (download.isPresent()) {
pendingSelectionCapture = null;
cachedSelectionBlueprint = download.get().blueprint();
cachedSelectionToken = download.get().cacheToken();
cachedSelectionAtNanos = System.nanoTime();
selectedBlueprint = cachedSelectionBlueprint;
blueprintToken = cachedSelectionToken;
transform = BlueprintTransform.NONE;
offset = BlockPos.ZERO;
lastPreviewAnchor = null;
} else BLUEPRINT_TRANSFERS.takeResult(selectionId).ifPresent(result -> {
if (!result.success()) {
pendingSelectionCapture = null;
showBlueprintMessage(result.message());
}
});
}
if (pendingPlacement != null) {
UUID placementId = pendingPlacement;
BLUEPRINT_TRANSFERS.takeResult(placementId).ifPresent(result -> {
pendingPlacement = null;
showBlueprintMessage(result.message());
});
}
}
/**
* Covers the brief interval in which the server's anti-spam capture cooldown is still active.
* A just-cancelled deployment can be restored locally from the capture that produced it; after
* two seconds we recapture normally so later source-world edits and cache eviction are respected.
*/
private boolean restoreFreshSelectionCopy() {
long age = System.nanoTime() - cachedSelectionAtNanos;
if (cachedSelectionBlueprint == null || cachedSelectionToken == null
|| cachedSelectionAtNanos == 0L || age < 0L || age > 2_000_000_000L) {
clearCachedSelectionCopy();
return false;
}
selectedBlueprint = cachedSelectionBlueprint;
blueprintToken = cachedSelectionToken;
transform = BlueprintTransform.NONE;
offset = BlockPos.ZERO;
lastPreviewAnchor = null;
blueprintTargetMissTicks = 0;
previewDirty = true;
return true;
}
private void clearCachedSelectionCopy() {
cachedSelectionBlueprint = null;
cachedSelectionToken = null;
cachedSelectionAtNanos = 0L;
}
private static void showBlueprintMessage(String message) {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player != null && message != null && !message.isBlank()) {
minecraft.player.displayClientMessage(Component.literal(message), false);
}
}
private void executeBlueprintAction(BlueprintAltActionController.Action action) {
if (action == BlueprintAltActionController.Action.COPY_SELECTION) {
requestSelectionCopy();
return;
}
if (action == BlueprintAltActionController.Action.SAVE) {
BuilderPreviewState.Snapshot preview = BuilderPreviewState.get().snapshot();
if (preview != null && preview.selection() != null && preview.selection().complete()) {
Minecraft minecraft = Minecraft.getInstance();
minecraft.setScreen(new BlueprintCaptureScreen(minecraft.screen));
}
return;
}
if (selectedBlueprint == null) return;
transform = switch (action) {
case FLIP_X -> new BlueprintTransform(!transform.flipX(), transform.flipY(), transform.flipZ(),
transform.rotationY());
case FLIP_Y -> new BlueprintTransform(transform.flipX(), !transform.flipY(), transform.flipZ(),
transform.rotationY());
case FLIP_Z -> new BlueprintTransform(transform.flipX(), transform.flipY(), !transform.flipZ(),
transform.rotationY());
case RESET -> BlueprintTransform.NONE;
default -> transform;
};
if (action == BlueprintAltActionController.Action.RESET) offset = BlockPos.ZERO;
previewDirty = true;
}
private void adjustBlueprint(BlueprintAltActionController.Action action, int direction) {
if (selectedBlueprint == null || direction == 0) return;
switch (action) {
case ROTATE_Y -> transform = new BlueprintTransform(transform.flipX(), transform.flipY(),
transform.flipZ(), rotate(transform.rotationY(), direction));
case OFFSET_X -> offset = offset.offset(direction, 0, 0);
case OFFSET_Y -> offset = offset.offset(0, direction, 0);
case OFFSET_Z -> offset = offset.offset(0, 0, direction);
default -> { return; }
}
previewDirty = true;
}
private static Rotation rotate(Rotation current, int direction) {
Rotation[] clockwise = { Rotation.NONE, Rotation.CLOCKWISE_90, Rotation.CLOCKWISE_180,
Rotation.COUNTERCLOCKWISE_90 };
int index = 0;
for (int i = 0; i < clockwise.length; i++) if (clockwise[i] == current) index = i;
return clockwise[Math.floorMod(index + direction, clockwise.length)];
}
private void refreshBlueprintPreview(boolean force) {
if (selectedBlueprint == null || blueprintToken == null) return;
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player == null || minecraft.level == null || minecraft.screen != null
|| !BuilderClientServices.isHoldingRealmwright(minecraft)) return;
BuilderClientCommand.Target airTarget = BuilderClientEvents.airPlacementTarget();
BlockPos anchor;
if (airTarget != null && airTarget.direct()) {
anchor = airTarget.blockPos().offset(offset);
} else {
HitResult picked = minecraft.player.pick(Math.max(1, snapshot().reach()), 1.0F, false);
if (!(picked instanceof BlockHitResult hit) || picked.getType() != HitResult.Type.BLOCK) {
if (lastPreviewAnchor != null) {
if (++blueprintTargetMissTicks >= 2) {
lastPreviewAnchor = null;
blueprintTargetMissTicks = 0;
clearBlueprintCells(minecraft);
}
}
return;
}
anchor = hit.getBlockPos().relative(hit.getDirection()).offset(offset);
}
blueprintTargetMissTicks = 0;
boolean currentGenerationIntact = BuilderPreviewState.get()
.hasCompleteDeploymentGeneration(selectedBlueprint.blocks().size());
if (!force && anchor.equals(lastPreviewAnchor) && currentGenerationIntact) return;
lastPreviewAnchor = anchor;
BlueprintPlacementPlan plan = BlueprintPlacementPlan.create(selectedBlueprint, transform, anchor);
List<BuilderPreviewState.Cell> cells = plan.blocks().stream().map(block -> {
net.minecraft.world.level.block.state.BlockState current = minecraft.level.getBlockState(block.worldPos());
BuilderPreviewState.Kind kind = current.equals(block.state())
? BuilderPreviewState.Kind.BLUEPRINT
: BuilderReplacementPolicy.canReplaceTarget(current, minecraft.level, block.worldPos(),
snapshot().allowReplacement(), minecraft.player.isCreative())
&& block.state().canSurvive(minecraft.level, block.worldPos())
&& !block.state().is(BuilderTags.CONSTRUCTION_PROTECTED)
&& !block.state().is(BuilderTags.TRANSACTION_UNSAFE)
? BuilderPreviewState.Kind.BUILD : BuilderPreviewState.Kind.INVALID;
// An already-matching BLUEPRINT cell is the real world block itself. Baking a
// second translucent model at the exact same coordinates causes depth fighting
// while the player moves; retain its blue outline only.
return new BuilderPreviewState.Cell(block.worldPos(), block.state(), kind,
kind == BuilderPreviewState.Kind.BUILD);
}).toList();
BuilderPreviewState.get().updateDeploymentPreview(minecraft.level.dimension(), cells);
}
private static void clearBlueprintCells(Minecraft minecraft) {
BuilderPreviewState.get().updateDeploymentPreview(minecraft.level.dimension(), List.of());
}
private void placeBlueprint(BuilderClientCommand.Target target) {
if (blueprintToken == null || selectedBlueprint == null) return;
BlockPos anchor = (target.direct() ? target.blockPos()
: target.blockPos().relative(target.face())).offset(offset);
UUID requestId = UUID.randomUUID();
pendingPlacement = requestId;
ModNetwork.CHANNEL.sendToServer(new BlueprintPlaceRequestPacket(requestId, blueprintToken,
anchor, transform));
}
}