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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.redstone.BundledRedstoneProvider;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.TurtleOrientationProvider;
import dan200.computercraft.api.turtle.TurtleRefuelHandler;
import dan200.computercraft.impl.ComputerCraftAPIService;
import net.minecraft.core.BlockPos;
Expand Down Expand Up @@ -203,6 +204,18 @@ public static void registerRefuelHandler(TurtleRefuelHandler handler) {
getInstance().registerRefuelHandler(handler);
}

/**
* Register an orientation provider for custom block placement transformations.
* This allows mod authors to define how their blocks should be oriented when
* turtles place them with directional parameters.
*
* @param provider The orientation provider to register.
* @see TurtleOrientationProvider
*/
public static void registerOrientationProvider(TurtleOrientationProvider provider) {
getInstance().registerOrientationProvider(provider);
}

private static ComputerCraftAPIService getInstance() {
return ComputerCraftAPIService.get();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright Daniel Ratcliffe, 2011-2022. This API may be redistributed unmodified and in full only.
//
// SPDX-License-Identifier: LicenseRef-CCPL

package dan200.computercraft.api.turtle;

import net.minecraft.core.Direction;
import net.minecraft.world.level.block.state.BlockState;
import org.jspecify.annotations.Nullable;

/**
* A provider for custom block orientation transformations during turtle placement.
* <p>
* This allows mod authors to register custom handling for their blocks when turtles
* place them with orientation parameters like "left", "up", etc.
*
* @see dan200.computercraft.api.ComputerCraftAPI#registerOrientationProvider
*/
@FunctionalInterface
public interface TurtleOrientationProvider {
/**
* Transform a block state based on turtle orientation parameters.
*
* @param state The original block state that was placed
* @param params The orientation parameters from the turtle.place call
* @return The transformed block state, or the original state if no transformation is needed.
* Return null to indicate this provider doesn't handle this block.
*/
@Nullable
BlockState transform(BlockState state, OrientationParameters params);

/**
* Parameters passed to turtle.place for controlling block orientation.
*/
public static final class OrientationParameters {
private final @Nullable Direction blockFacing;
private final @Nullable Boolean isTop;
private final boolean isUpsideDown;
private final boolean isGroundAttachment;
private final @Nullable Direction clickFace;

public OrientationParameters(@Nullable Direction blockFacing,
@Nullable Boolean isTop,
boolean isUpsideDown,
boolean isGroundAttachment,
@Nullable Direction clickFace) {
this.blockFacing = blockFacing;
this.isTop = isTop;
this.isUpsideDown = isUpsideDown;
this.isGroundAttachment = isGroundAttachment;
this.clickFace = clickFace;
}

/**
* The direction the block should face, or null if no facing was specified.
*/
public @Nullable Direction getBlockFacing() {
return blockFacing;
}

/**
* Whether the block should be on top (true), bottom (false), or unspecified (null).
* Primarily used for slabs.
*/
public @Nullable Boolean getIsTop() {
return isTop;
}

/**
* Whether the block should be placed upside down (for stairs, etc.).
*/
public boolean isUpsideDown() {
return isUpsideDown;
}

/**
* Whether this is a ground attachment (for torches, etc.).
*/
public boolean isGroundAttachment() {
return isGroundAttachment;
}

/**
* Which face of the target block to click, or null if not specified.
*/
public @Nullable Direction getClickFace() {
return clickFace;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.pocket.PocketUpgradeSerialiser;
import dan200.computercraft.api.redstone.BundledRedstoneProvider;
import dan200.computercraft.api.turtle.TurtleOrientationProvider;
import dan200.computercraft.api.turtle.TurtleRefuelHandler;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import net.minecraft.core.BlockPos;
Expand Down Expand Up @@ -67,6 +68,8 @@ static ComputerCraftAPIService get() {

void registerRefuelHandler(TurtleRefuelHandler handler);

void registerOrientationProvider(TurtleOrientationProvider provider);

ResourceKey<Registry<TurtleUpgradeSerialiser<?>>> turtleUpgradeRegistryId();

ResourceKey<Registry<PocketUpgradeSerialiser<?>>> pocketUpgradeRegistryId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.pocket.PocketUpgradeSerialiser;
import dan200.computercraft.api.redstone.BundledRedstoneProvider;
import dan200.computercraft.api.turtle.TurtleOrientationProvider;
import dan200.computercraft.api.turtle.TurtleRefuelHandler;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.core.filesystem.WritableFileMount;
Expand Down Expand Up @@ -112,6 +113,11 @@ public final void registerRefuelHandler(TurtleRefuelHandler handler) {
TurtleRefuelHandlers.register(handler);
}

@Override
public final void registerOrientationProvider(TurtleOrientationProvider provider) {
TurtleOrientationProviders.register(provider);
}

@Override
public final ResourceKey<Registry<TurtleUpgradeSerialiser<?>>> turtleUpgradeRegistryId() {
return turtleUpgradeRegistryId;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0

package dan200.computercraft.impl;

import dan200.computercraft.api.turtle.TurtleOrientationProvider;
import net.minecraft.world.level.block.state.BlockState;
import org.jspecify.annotations.Nullable;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;

/**
* Registry of {@link TurtleOrientationProvider}s.
*/
public final class TurtleOrientationProviders {
private static final List<TurtleOrientationProvider> providers = new CopyOnWriteArrayList<>();

private TurtleOrientationProviders() {
}

public static synchronized void register(TurtleOrientationProvider provider) {
Objects.requireNonNull(provider, "provider cannot be null");
providers.add(provider);
}

@Nullable
public static BlockState transform(BlockState state, TurtleOrientationProvider.OrientationParameters params) {
for (var provider : providers) {
var result = provider.transform(state, params);
if (result != null) {
return result;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.shared.peripheral.generic.methods.AbstractInventoryMethods;
import dan200.computercraft.shared.turtle.core.*;
import net.minecraft.core.Direction;
import org.jspecify.annotations.Nullable;

import java.util.Locale;
import java.util.Map;
import java.util.Optional;

Expand Down Expand Up @@ -221,13 +223,19 @@ public final MethodResult digDown(Optional<TurtleSide> side) {
* @param args Arguments to place.
* @return The turtle command result.
* @cc.tparam [opt] string text When placing a sign, set its contents to this text.
* @cc.tparam [opt] table options Table with placement options: { facing = "left", position = "top", text = "Hello" }.
* The "facing" field can be a string of a relative direction left, right, up, down, forward, backward.
* The "position" field sets slab/stair positioning/upside down with top ("upsidedown"), bottom as well as ground for torch like items that shouldnt be placed on a wall.
* The "text" field sets sign text.
* if you just pass a string it is passed like it normally would be to a sign.
* @cc.treturn boolean Whether the block could be placed.
* @cc.treturn string|nil The reason the block was not placed.
* @cc.since 1.4
* @cc.changed 1.116.1 Added table-based placement options for cleaner API usage.
*/
@LuaFunction
public final MethodResult place(IArguments args) throws LuaException {
return trackCommand(new TurtlePlaceCommand(InteractDirection.FORWARD, args.getAll()));
return trackCommand(new TurtlePlaceCommand(InteractDirection.FORWARD, parseArgumentsToParameters(args)));
}

/**
Expand All @@ -243,7 +251,7 @@ public final MethodResult place(IArguments args) throws LuaException {
*/
@LuaFunction
public final MethodResult placeUp(IArguments args) throws LuaException {
return trackCommand(new TurtlePlaceCommand(InteractDirection.UP, args.getAll()));
return trackCommand(new TurtlePlaceCommand(InteractDirection.UP, parseArgumentsToParameters(args)));
}

/**
Expand All @@ -259,7 +267,7 @@ public final MethodResult placeUp(IArguments args) throws LuaException {
*/
@LuaFunction
public final MethodResult placeDown(IArguments args) throws LuaException {
return trackCommand(new TurtlePlaceCommand(InteractDirection.DOWN, args.getAll()));
return trackCommand(new TurtlePlaceCommand(InteractDirection.DOWN, parseArgumentsToParameters(args)));
}

/**
Expand Down Expand Up @@ -824,4 +832,115 @@ private static int checkCount(Optional<Integer> countArg) throws LuaException {
if (count < 0 || count > 64) throw new LuaException("Item count " + count + " out of range");
return count;
}

/**
* Parse table and string based arguments for turtle.place
* returning structured PlacementParameters.
*/
private TurtlePlaceCommand.PlacementParameters parseArgumentsToParameters(IArguments args) throws LuaException {
if (args.count() == 0) {
return new TurtlePlaceCommand.PlacementParameters(null, null, false, false, null, null);
}

// Check if first argument is a table
var firstArg = args.get(0);
if (firstArg instanceof Map<?, ?> table) {
return parseTableToParameters(table);
}

// Default sign handling
return new TurtlePlaceCommand.PlacementParameters(null, null, false, false, null, args.getString(0));
}

private TurtlePlaceCommand.PlacementParameters parseTableToParameters(Map<?, ?> table) throws LuaException {
Direction blockFacing = null;
Boolean isTop = null;
boolean isUpsideDown = false;
boolean isGroundAttachment = false;
Direction clickFace = null;
String signText = null;

// Handle "text" field for signs
if (table.containsKey("text")) {
var text = table.get("text");
if (text instanceof String string) {
signText = string;
} else {
throw new LuaException("Expected string value for 'text' field");
}
}

// Handle "facing" field
if (table.containsKey("facing")) {
var facing = table.get("facing");
if (facing instanceof String string) {
var parsed = parseDirectionParameter(string);
blockFacing = parsed.blockFacing;
if (parsed.isUpsideDown) isUpsideDown = true;
if (parsed.clickFace != null) clickFace = parsed.clickFace;
if (parsed.isGroundAttachment) isGroundAttachment = true;
} else {
throw new LuaException("Expected string value for 'facing' field");
}
}

// Handle "position" field for slabs/stairs
if (table.containsKey("position")) {
var position = table.get("position");
if (position instanceof String posStr) {
switch (posStr.toLowerCase(Locale.ROOT)) {
// top is reused here, it can set both itTop and isUpsideDown since blocks that would use these should'nt be affected by ones it doesnt use.
case "top" -> {
isTop = true;
isUpsideDown = true;
}
// included for completeness but this is the default behaviour, remove?
case "bottom" -> isTop = false;
//torch like item specific so that it isnt placed against a wall.
case "ground" -> isGroundAttachment = true;
default -> throw new LuaException("Unknown position value: " + posStr);
}
} else {
throw new LuaException("Expected string value for 'position' field");
}
}

return new TurtlePlaceCommand.PlacementParameters(blockFacing, isTop, isUpsideDown, isGroundAttachment, clickFace, signText);
}

private record ParsedDirection(
@Nullable Direction blockFacing,
@Nullable Boolean isTop,
boolean isUpsideDown,
boolean isGroundAttachment,
@Nullable Direction clickFace
) {}

private ParsedDirection parseDirectionParameter(String orientation) {
Direction blockFacing = null;
Boolean isTop = null;
boolean isUpsideDown = false;
boolean isGroundAttachment = false;
Direction clickFace = null;

switch (orientation.toLowerCase(Locale.ROOT)) {
case "up" -> {
clickFace = Direction.DOWN;
blockFacing = Direction.UP;
}
case "down" -> {
clickFace = Direction.UP;
blockFacing = Direction.DOWN;
}
case "forward" -> blockFacing = turtle.getDirection();
case "left" -> blockFacing = turtle.getDirection().getCounterClockWise();
case "right" -> blockFacing = turtle.getDirection().getClockWise();
case "back", "backward" -> blockFacing = turtle.getDirection().getOpposite();
case "top" -> isTop = true;
case "bottom" -> isTop = false;
case "ground" -> isGroundAttachment = true;
}

return new ParsedDirection(blockFacing, isTop, isUpsideDown, isGroundAttachment, clickFace);
}
}
Loading
Loading