Skip to content

Create Your Own Ink Action

Ink Actions define custom narrative commands that follow a validate → execute workflow.
Validation (doValidate) parses arguments and initializes variables, while execution (doExecute) performs the actual logic.
Each action must return an InkActionResult, indicating success, warning, or error.


Basic Structure

java
public class MyCustomInkAction extends InkAction {
    private String targetValue;
    private boolean immediateMode;

    public MyCustomInkAction(String id, Side side, String syntax, CommandMatcher matcher) {
        super(id, side, syntax, matcher);
    }

    @Override
    protected InkActionResult doValidate(List<String> arguments, Scene scene) {
        if (arguments.size() < 2)
            return InkActionResult.error("Missing argument: target value");

        targetValue = arguments.get(1);
        if (targetValue.isEmpty())
            return InkActionResult.error("Target value cannot be empty");

        if (arguments.size() > 2) {
            try {
                immediateMode = Boolean.parseBoolean(arguments.get(2));
            } catch (Exception e) {
                return InkActionResult.error("Invalid boolean: " + arguments.get(2));
            }
        }

        return InkActionResult.ok();
    }

    @Override
    protected InkActionResult doExecute(PlayerSession session) {
        if (targetValue == null || targetValue.isEmpty())
            return InkActionResult.error("Target value not set");

        if (immediateMode) {
            applyTargetValue(session, targetValue);
            isRunning = false;
        } else {
            initializeGradualProcess(session, targetValue);
        }

        return InkActionResult.ok();
    }
}

Registration

java
InkActionRegistry.register(() -> new MyCustomInkAction(
    "my_custom_action",
    InkAction.Side.SERVER,
    "mycmd %target% [immediate]",
    command -> command.startsWith("mycmd")
));

Example

java
public class TeleportInkAction extends InkAction {
    private double x, y, z;
    private boolean relative = false;
    private String dimension = null;

    public TeleportInkAction(String id, Side side, String syntax, CommandMatcher matcher) {
        super(id, side, syntax, matcher);
    }

    @Override
    protected InkActionResult doValidate(List<String> args, Scene scene) {
        if (args.size() < 4)
            return InkActionResult.error("Missing coordinates");

        try {
            x = Double.parseDouble(args.get(1));
            y = Double.parseDouble(args.get(2));
            z = Double.parseDouble(args.get(3));
        } catch (NumberFormatException e) {
            return InkActionResult.error("Coordinates must be numbers");
        }

        if (args.size() > 4)
            relative = Boolean.parseBoolean(args.get(4));
        if (args.size() > 5)
            dimension = args.get(5);

        return InkActionResult.ok();
    }

    @Override
    protected InkActionResult doExecute(PlayerSession session) {
        Player player = session.getPlayer();

        double finalX = relative ? player.getX() + x : x;
        double finalY = relative ? player.getY() + y : y;
        double finalZ = relative ? player.getZ() + z : z;

        if (dimension != null) {
            ResourceKey<Level> targetDimension = switch (dimension) {
                case "nether" -> Level.NETHER;
                case "end" -> Level.END;
                default -> Level.OVERWORLD;
            };

            if (!player.level().dimension().equals(targetDimension))
                return InkActionResult.warn("Cross-dimension teleport initiated");
        }

        player.teleportTo(finalX, finalY, finalZ);
        isRunning = false;
        return InkActionResult.ok();
    }

    @Override
    public boolean needScene() {
        return false;
    }
}

// Registration
InkActionRegistry.register(() -> new TeleportInkAction(
    "teleport",
    InkAction.Side.SERVER,
    "tp %x% %y% %z% [relative] [dimension]",
    command -> command.startsWith("tp ")
));