Skip to content
Merged
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 @@ -1138,7 +1138,8 @@ private void handleDispatchOutcome(
return;
}

sendCompletedCommand(player, command);
sendCompletedCommand(
player, outcome.rewrittenCommand() != null ? outcome.rewrittenCommand() : command);

runPostActions(
player,
Expand All @@ -1159,12 +1160,20 @@ private void handleDispatchOutcome(
error != null && error.detail() != null ? error.detail() : "post-action failed");
});
} else {
String detail =
outcome != null && outcome.error() != null ? outcome.error().detail() : "dispatch failed";
reportCommandFailure(player, executionId, initiatorUuid, detail);
// A listener cancelling the command is not an error, so the player gets no error message.
boolean cancelledByListener = outcome != null && outcome.isCancelledByListener();
ExecutionStage terminalStage =
cancelledByListener ? ExecutionStage.CANCELLED : ExecutionStage.ERROR;
if (!cancelledByListener) {
String detail =
outcome != null && outcome.error() != null
? outcome.error().detail()
: "dispatch failed";
reportCommandFailure(player, executionId, initiatorUuid, detail);
}

if (!instance.tryTransitionTo(ExecutionStage.POST_ACTIONS)) {
instance.tryTransitionTo(ExecutionStage.ERROR);
instance.tryTransitionTo(terminalStage);
registry.removeIfExact(initiatorUuid, executionId);
return;
}
Expand All @@ -1175,11 +1184,11 @@ private void handleDispatchOutcome(
initiatorExecutor,
ExecutionPolicy.ON_CANCEL,
() -> {
instance.tryTransitionTo(ExecutionStage.ERROR);
instance.tryTransitionTo(terminalStage);
registry.removeIfExact(initiatorUuid, executionId);
},
error -> {
instance.tryTransitionTo(ExecutionStage.ERROR);
instance.tryTransitionTo(terminalStage);
registry.removeIfExact(initiatorUuid, executionId);
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dev.cyr1en.promptpaper.execution.dispatch;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;

/**
* Fires a {@link PlayerCommandPreprocessEvent} for a finished prompt command before it is
* dispatched, so plugins that watch typed commands (alerts, logs, aliases) also see commands that
* went through a prompt. The original event was cancelled when the prompt opened.
*/
public final class CommandEventBridge {

private static final ThreadLocal<PlayerCommandPreprocessEvent> CURRENT = new ThreadLocal<>();

private CommandEventBridge() {}

/**
* True only for the event the bridge is firing, so the prompt listener can skip it. Any other
* command event raised by a listener during that call is still treated normally.
*/
public static boolean isBridgeEvent(PlayerCommandPreprocessEvent event) {
return event != null && CURRENT.get() == event;
}

/**
* Fires the event and returns the command to run (without the leading slash), or null when a
* listener cancelled it.
*/
public static String fire(Player player, String command) {
PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(player, "/" + command);
PlayerCommandPreprocessEvent previous = CURRENT.get();
CURRENT.set(event);
try {
Bukkit.getPluginManager().callEvent(event);
} finally {
if (previous == null) {
CURRENT.remove();
} else {
CURRENT.set(previous);
}
}
if (event.isCancelled()) {
return null;
}
return DispatchSanitizer.stripLeadingSlash(event.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ public enum DispatchErrorKind {
/** Bukkit dispatch threw an unhandled exception. */
EXCEPTION_THROWN,
/** Target or initiator player entity scheduler was retired or unavailable. */
SCHEDULER_RETIRED
SCHEDULER_RETIRED,
/** A command event listener cancelled the finished command, so it never ran. */
CANCELLED_BY_LISTENER
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,29 @@
import java.util.Optional;

/** Immutable result of a command dispatch invocation. */
public record DispatchOutcome(boolean isSuccess, DispatchError error) {
public record DispatchOutcome(boolean isSuccess, DispatchError error, String rewrittenCommand) {
public DispatchOutcome {
if (!isSuccess && error == null) {
error = DispatchError.of(DispatchErrorKind.DISPATCH_RETURNED_FALSE, "dispatch failed");
}
}

public static DispatchOutcome success() {
return new DispatchOutcome(true, null);
return new DispatchOutcome(true, null, null);
}

/** Success for a command that a listener replaced; carries the command that actually ran. */
public static DispatchOutcome success(String rewrittenCommand) {
return new DispatchOutcome(true, null, rewrittenCommand);
}

public static DispatchOutcome cancelledByListener() {
return failure(DispatchErrorKind.CANCELLED_BY_LISTENER, "cancelled by a command listener");
}

public static DispatchOutcome failure(DispatchError error) {
Objects.requireNonNull(error, "error must not be null for failure outcome");
return new DispatchOutcome(false, error);
return new DispatchOutcome(false, error, null);
}

public static DispatchOutcome failure(DispatchErrorKind kind, String detail) {
Expand All @@ -32,6 +41,10 @@ public boolean isFailure() {
return !isSuccess;
}

public boolean isCancelledByListener() {
return error != null && error.kind() == DispatchErrorKind.CANCELLED_BY_LISTENER;
}

public Optional<DispatchError> optionalError() {
return Optional.ofNullable(error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import dev.cyr1en.promptpaper.util.Scheduler;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.permissions.PermissionAttachment;
Expand Down Expand Up @@ -79,25 +80,46 @@ private void dispatchPlayer(
PlayerExecutor initiator, Player player, String command, PrimaryDispatchCallback callback) {
initiator.execute(
() -> {
DispatchOutcome outcome;
try {
boolean ok = Bukkit.dispatchCommand(player, command);
if (ok) {
callback.onComplete(DispatchOutcome.success());
} else {
callback.onComplete(
DispatchOutcome.failure(
DispatchErrorKind.DISPATCH_RETURNED_FALSE, "dispatch returned false"));
}
outcome = fireAndDispatch(player, command, () -> runAsPlayer(player, command));
} catch (Throwable t) {
callback.onComplete(DispatchOutcome.failure(DispatchErrorKind.EXCEPTION_THROWN, t));
outcome = DispatchOutcome.failure(DispatchErrorKind.EXCEPTION_THROWN, t);
}
callback.onComplete(outcome);
},
() ->
callback.onComplete(
DispatchOutcome.failure(
DispatchErrorKind.SCHEDULER_RETIRED, "player scheduler retired")));
}

/**
* Fires the command event, then runs the command. An unchanged command goes to {@code
* runUnchanged}; a command a listener replaced always runs as the plain player, because any
* temporary permissions were granted for the original command only.
*/
private DispatchOutcome fireAndDispatch(
Player player, String command, Supplier<DispatchOutcome> runUnchanged) {
String finalCommand = CommandEventBridge.fire(player, command);
if (finalCommand == null) {
return DispatchOutcome.cancelledByListener();
}
if (finalCommand.equals(command)) {
return runUnchanged.get();
}
DispatchOutcome outcome = runAsPlayer(player, finalCommand);
return outcome.isSuccess() ? DispatchOutcome.success(finalCommand) : outcome;
}

private DispatchOutcome runAsPlayer(Player player, String command) {
if (Bukkit.dispatchCommand(player, command)) {
return DispatchOutcome.success();
}
return DispatchOutcome.failure(
DispatchErrorKind.DISPATCH_RETURNED_FALSE, "dispatch returned false");
}

private void dispatchConsole(String command, PrimaryDispatchCallback callback) {
try {
scheduler.runSync(
Expand Down Expand Up @@ -146,62 +168,51 @@ private void dispatchAttachment(

initiator.execute(
() -> {
PermissionAttachment attachment;
try {
attachment = player.addAttachment(plugin);
} catch (Throwable t) {
callback.onComplete(
DispatchOutcome.failure(DispatchErrorKind.ATTACHMENT_CREATION_FAILED, t));
return;
}

if (attachment == null) {
callback.onComplete(
DispatchOutcome.failure(
DispatchErrorKind.ATTACHMENT_CREATION_FAILED,
"unable to create permission attachment"));
return;
}

final PermissionAttachment finalAttachment = attachment;
AtomicBoolean removed = new AtomicBoolean(false);
Runnable removeTask =
() -> {
if (removed.compareAndSet(false, true)) {
try {
player.removeAttachment(finalAttachment);
} catch (Throwable ignored) {
}
}
};

DispatchOutcome outcome;
try {
for (String perm : context.permissionSnapshot()) {
attachment.setPermission(perm, true);
}
attachment.getPermissible().recalculatePermissions();

boolean ok = Bukkit.dispatchCommand(player, command);
if (ok) {
outcome = DispatchOutcome.success();
} else {
outcome =
DispatchOutcome.failure(
DispatchErrorKind.DISPATCH_RETURNED_FALSE, "dispatch returned false");
}
// The event fires before the grant so listeners see the player's real permissions.
outcome =
fireAndDispatch(player, command, () -> runWithAttachment(player, command, context));
} catch (Throwable t) {
outcome = DispatchOutcome.failure(DispatchErrorKind.EXCEPTION_THROWN, t);
}

// The attachment exists only for the synchronous dispatch call. Completion is
// never published while elevated permissions remain attached.
removeTask.run();
callback.onComplete(outcome);
},
() ->
callback.onComplete(
DispatchOutcome.failure(
DispatchErrorKind.SCHEDULER_RETIRED, "player scheduler retired")));
}

private DispatchOutcome runWithAttachment(
Player player, String command, PermissionAttachmentContext context) {
PermissionAttachment attachment;
try {
attachment = player.addAttachment(plugin);
} catch (Throwable t) {
return DispatchOutcome.failure(DispatchErrorKind.ATTACHMENT_CREATION_FAILED, t);
}

if (attachment == null) {
return DispatchOutcome.failure(
DispatchErrorKind.ATTACHMENT_CREATION_FAILED, "unable to create permission attachment");
}

// The attachment exists only for the synchronous dispatch call. Completion is
// never published while elevated permissions remain attached.
try {
for (String perm : context.permissionSnapshot()) {
attachment.setPermission(perm, true);
}
attachment.getPermissible().recalculatePermissions();
return runAsPlayer(player, command);
} catch (Throwable t) {
return DispatchOutcome.failure(DispatchErrorKind.EXCEPTION_THROWN, t);
} finally {
try {
player.removeAttachment(attachment);
} catch (Throwable ignored) {
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dev.cyr1en.promptpaper.CommandPrompter;
import dev.cyr1en.promptpaper.engine.InterceptResult;
import dev.cyr1en.promptpaper.engine.PromptEngine;
import dev.cyr1en.promptpaper.execution.dispatch.CommandEventBridge;
import dev.cyr1en.promptpaper.screen.ScreenManager;
import java.util.Locale;
import org.bukkit.event.EventHandler;
Expand Down Expand Up @@ -52,6 +53,10 @@ public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
plugin.getPluginLogger().debug("Command already cancelled, skipping");
return;
}
if (CommandEventBridge.isBridgeEvent(event)) {
// Finished prompt command, re-fired for other plugins. Never prompt it again.
return;
}
var player = event.getPlayer();
var message = event.getMessage();
var commandName =
Expand Down
Loading