Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make NativeProxyCommandSender extend CraftBukkit class ProxiedNativeCommandSender #478

Open
wants to merge 8 commits into
base: dev/10.0.0
Choose a base branch
from

Conversation

willkroboth
Copy link
Collaborator

@willkroboth willkroboth commented Aug 7, 2023

This fixes #477. See that issue for context and a deeper analysis of the problem.

When VanillaCommandWrapper runs a command, it uses its getListener method to convert a Bukkit CommandSender into a Vanilla CommandListenerWrapper. When the sender implements org.bukkit.command.ProxiedCommandSender, it runs this code:

if (sender instanceof ProxiedCommandSender) {
	return ((ProxiedNativeCommandSender) sender).getHandle();
}

Previously, dev.jorel.commandapi.wrappers.NativeProxyCommandSender implemented ProxiedCommandSender, but not ProxiedNativeCommandSender, so a ClassCastException was thrown if a NativeProxyCommandSender ever tried to execute a Vanilla command.

To fix this issue, instances of NativeProxyCommandSender need to be ProxiedNativeCommandSender objects. This isn't trivial to do, since ProxiedNativeCommandSender is a version-specific CraftBukkit class (org.bukkit.craftbukkit.v1_20_R1.command.ProxiedNativeCommandSender for example). It is still possible, you just need to create an implementation of NativeProxyCommandSender for each NMS version, and that is what this PR does.


NativeProxyCommandSender is now an interface, like this:

public interface NativeProxyCommandSender extends ProxiedCommandSender {
	Location getLocation();

	World getWorld();
}

It still inherits all the methods from ProxiedCommandSender and adds the getLocation and getWorld methods, as described in the docs.

In each of the commandapi-bukkit-nms modules, there is now a NativeProxyCommandSender_(VERSION) class. For example, there is this class in commandapi-bukkit-nms-1.20:

import org.bukkit.craftbukkit.v1_20_R1.command.ProxiedNativeCommandSender;

public class NativeProxyCommandSender_1_20_R1 extends ProxiedNativeCommandSender implements NativeProxyCommandSender {
	private final World world;
	private final Location location;

	public NativeProxyCommandSender_1_20_R1(CommandSourceStack css, CommandSender caller, CommandSender callee) {
		super(css, caller, callee);

		Vec3 pos = css.getPosition();
		Vec2 rot = css.getRotation();
		this.world = CommandAPIBukkit.get().getWorldForCSS(css);
		this.location = new Location(this.world, pos.x(), pos.y(), pos.z(), rot.y, rot.x);
	}

	@Override
	public Location getLocation() {
		return location;
	}

	@Override
	public World getWorld() {
		return world;
	}
}

The logic for creating a Location and World from the CommandSourceStack was moved from the implementation of CommandAPIBukkit#getSenderForCommand. Extending ProxiedNativeCommandSender takes care of the implementation for all the ProxiedCommandSender methods (I checked and they look equivalent to the implementations that used to be in NativeProxyCommandSender), so these classes only have to implement getLocation and getWorld.

Luckily, NativeProxyCommandSender was being constructed in the NMS implementations anyway, so it was easy to change those:

@Override
public BukkitCommandSender<? extends CommandSender> getSenderForCommand(CommandContext<CommandSourceStack> cmdCtx, boolean isNative) {
	CommandSourceStack css = cmdCtx.getSource();

	CommandSender sender = css.getBukkitSender();
	if (sender == null) {
		// Sender CANNOT be null. This can occur when using a remote console
		// sender. You can access it directly using this.<MinecraftServer>getMinecraftServer().remoteConsole
		// however this may also be null, so delegate to the next most-meaningful sender.
		sender = Bukkit.getConsoleSender();
	}

	Entity proxyEntity = css.getEntity();
	CommandSender proxy = proxyEntity == null ? null : proxyEntity.getBukkitEntity();
	if (isNative || (proxy != null && !sender.equals(proxy))) {
		if (proxy == null) {
			proxy = sender;
		}
		return new BukkitNativeProxyCommandSender(new NativeProxyCommandSender_1_20_R1(css, sender, proxy));
	} else {
		return wrapCommandSender(sender);
	}
}

Instead of constructing a NativeProxyCommandSender directly, each NMS uses its version-specific implementation. The logic for extracting the Location and World has also been moved into the constructor for NativeProxyCommandSender_(VERSION), so it isn't in here anymore.


IMPORTANT: This is a breaking API change

Unfortunately, NativeProxyCommandSender cannot be a class anymore, because ProxiedNativeCommandSender is a class, and Java doesn't have multi-class inheritance. This is problematic in two ways.

First, developers could have created their own instances of NativeProxyCommandSender with something like new NativeProxyCommandSender(caller, callee, location, world). Since NativeProxyCommandSender is now an interface, it can't be instantiated like that anymore. I wouldn't expect anyone to do that, since usually you are just given a NativeProxyCommandSender when using executesNative, but it might be a problem.

Second, a java.lang.IncompatibleClassChangeError occurs when using the plugin version of the CommandAPI. For example, say I compile this command against version 9.0.3:

new CommandAPICommand("test")
        .executesNative((sender, args) -> {
            sender.getCallee().sendMessage("You are the target of the test command");
        })
        .register();

Notably, this code uses the method ProxiedCommandSender#getCallee on a NativeProxyCommandSender object. If I try to run this command using the CommandAPI plugin jar for this PR, I get this exception:

[Server thread/ERROR]: [CommandAPI] Unhandled exception executing '/test'
java.lang.IncompatibleClassChangeError: Found interface dev.jorel.commandapi.wrappers.NativeProxyCommandSender, but class was expected
        at me.willkroboth.CommandAPITest.Main.lambda$onEnable$0(Main.java:24) ~[?:?]
        at dev.jorel.commandapi.executors.NativeCommandExecutor.run(NativeCommandExecutor.java:49) ~[?:?]
        at dev.jorel.commandapi.executors.NormalExecutor.executeWith(NormalExecutor.java:44) ~[?:?]
        at dev.jorel.commandapi.CommandAPIExecutor.execute(CommandAPIExecutor.java:138) ~[?:?]
        at dev.jorel.commandapi.CommandAPIExecutor.execute(CommandAPIExecutor.java:113) ~[?:?]
        at dev.jorel.commandapi.CommandAPIExecutor.execute(CommandAPIExecutor.java:96) ~[?:?]
        at dev.jorel.commandapi.CommandAPIHandler.lambda$generateCommand$0(CommandAPIHandler.java:258) ~[?:?]
        at com.mojang.brigadier.CommandDispatcher.execute(CommandDispatcher.java:265) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:?]
        at net.minecraft.commands.CommandDispatcher.performCommand(CommandDispatcher.java:314) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at net.minecraft.commands.CommandDispatcher.performPrefixedCommand(CommandDispatcher.java:293) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at org.bukkit.craftbukkit.v1_20_R1.command.VanillaCommandWrapper.execute(VanillaCommandWrapper.java:45) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at org.bukkit.command.SimpleCommandMap.dispatch(SimpleCommandMap.java:149) ~[spigot-api-1.20.1-R0.1-SNAPSHOT.jar:?]
        at org.bukkit.craftbukkit.v1_20_R1.CraftServer.dispatchCommand(CraftServer.java:875) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at org.bukkit.craftbukkit.v1_20_R1.CraftServer.dispatchServerCommand(CraftServer.java:860) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at net.minecraft.server.dedicated.DedicatedServer.bf(DedicatedServer.java:413) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at net.minecraft.server.dedicated.DedicatedServer.b(DedicatedServer.java:389) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at net.minecraft.server.MinecraftServer.a(MinecraftServer.java:1198) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at net.minecraft.server.MinecraftServer.w(MinecraftServer.java:1015) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at net.minecraft.server.MinecraftServer.lambda$0(MinecraftServer.java:304) ~[spigot-1.20.1-R0.1-SNAPSHOT.jar:3838-Spigot-3374045-a2fafdd]
        at java.lang.Thread.run(Thread.java:833) ~[?:?]
[Server thread/INFO]: An unexpected error occurred trying to execute that command

As the exception explains, the bytecode for my command expected NativeProxyCommandSender to be a class, but it found an interface instead. To fix this, I just need to recompile my plugin since the available methods don't change. Still, code compiled against older versions may not work with this PR if they use methods in NativeProxyCommandSender.

There might be some way to avoid these issues, but for now, this PR is incompatible with certain interactions with NativeProxyCommandSender.


TODO:

  • Apply the changes to the test framework (whoops, forgot that)

  • Make sure tests run as before

  • Test NMS code on real servers

    • getSenderForCommand
    • createNativeProxyCommandSender
versions
  • 1.15, 1.15.1, 1.15.2
  • 1.16.1
  • 1.16.2, 1.16.3
  • 1.16.4
  • 1.16.5
  • 1.17
  • 1.17.1
  • 1.18, 1.18.1
  • 1.18.2
  • 1.19
  • 1.19.1, 1.19.2
  • 1.19.3
  • 1.19.4
  • 1.20, 1.20.1
  • 1.20.2

@willkroboth willkroboth marked this pull request as ready for review August 7, 2023 18:50
Copy link
Owner

@JorelAli JorelAli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Seems like something we're more than happy to add to a 10.0.0 release due to backwards-incompatible changes.

I think I should probably start making a separate branch for 10.0.0 stuff because we've got a lot of backwards-incompatible PRs that we can't/shouldn't merge to dev/dev in the event we want to release 9.10.0 (e.g. for the Folia fix and/or upcoming Minecraft versions)

@JorelAli
Copy link
Owner

JorelAli commented Aug 8, 2023

Created branch dev/10.0.0 for backwards-incompatible changes. Please ensure this PR is retargeted to dev/10.0.0 instead of dev/dev before merging.

@willkroboth willkroboth changed the base branch from dev/dev to dev/10.0.0 August 8, 2023 15:36
@willkroboth
Copy link
Collaborator Author

willkroboth commented Aug 8, 2023

First, developers could have created their own instances of NativeProxyCommandSender with something like new NativeProxyCommandSender(caller, callee, location, world). Since NativeProxyCommandSender is now an interface, it can't be instantiated like that anymore. I wouldn't expect anyone to do that, since usually you are just given a NativeProxyCommandSender when using executesNative, but it might be a problem.

There should be a new way for developers to create their own NativeProxyCommandSenders with this information.

TODO:

@willkroboth willkroboth linked an issue Aug 8, 2023 that may be closed by this pull request
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch 2 times, most recently from 9d90668 to 423df48 Compare August 12, 2023 20:15
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from 2b438f3 to eb4b6e8 Compare August 17, 2023 21:35
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from eb4b6e8 to 3327ff6 Compare September 22, 2023 20:58
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from 3327ff6 to b3d6965 Compare November 11, 2023 23:16
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from b3d6965 to f18c081 Compare December 16, 2023 01:15
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch 2 times, most recently from 84b81e7 to a75e4e4 Compare January 1, 2024 14:47
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from a75e4e4 to 7b09a17 Compare February 26, 2024 14:24
willkroboth added a commit to willkroboth/CommandAPI that referenced this pull request Feb 26, 2024
willkroboth added a commit to willkroboth/CommandAPI that referenced this pull request Mar 31, 2024
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from 20095f3 to bebca9c Compare March 31, 2024 23:38
willkroboth added a commit to willkroboth/CommandAPI that referenced this pull request Apr 13, 2024
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from bebca9c to 53ae7f1 Compare April 13, 2024 12:44
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from 53ae7f1 to 9836d19 Compare May 1, 2024 14:58
willkroboth added a commit to willkroboth/CommandAPI that referenced this pull request May 1, 2024
willkroboth added a commit to willkroboth/CommandAPI that referenced this pull request May 14, 2024
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from 9836d19 to 17d208c Compare May 14, 2024 13:32
@willkroboth willkroboth force-pushed the dev/fix-ProxiedNativeCommandSender branch from 17d208c to 32bbe6d Compare May 14, 2024 14:00
willkroboth added a commit that referenced this pull request May 17, 2024
Note: This commit makes this branch backwards incompatible, especially for non-DSL Kotlin code (see `commandapi-documentation-code/.../Examples.kt`). "Standard" Java and DSL API code usage looks the same, but I'm pretty sure plugins will need to recompile due to changes to the `FunctionalInterface`s. There are also some smaller public API changes, mentioned below.

Notable changes:
- Removed `AbstractCommandSender` and all its implemenations (i.e. the `dev.jorel.commandapi.commandsenders` package is completely gone)
  - Logic in `CommandAPIHandler#generateBrigadierRequirements` for checking if a sender satisfies a `CommandPermission` was moved to `CommandAPIPlatform#getPermissionCheck`
    - Previously, methods in `AbstractCommandSender` provided access to `hasPermission` and `isOp`. `CommandAPIBukkit` and `CommandAPIVelocity` now handle these definitions.
    -  `CommandPermission.TRUE()` and `CommandPermission.FALSE()` added for computing short circuits when combining permissions and requirements
  - `PreviewInfo` now has the generic parameter `Player` rather than an `AbstractPlayer`
    - Backwards-incompatible (`(Player) player.getSource()` becomes `player`)
    - Generic parameter propogates to `PreviewableFunction` and `Previewable`
  - `CommandAPIPlatform` methods for convert Brigadier Source to CommandSender simplified
    - `getSenderForCommand` removed
      - just pass `CommandContext#getSource` into `getCommandSenderFromCommandSource`
      - `forceNative` parameter was only used on Bukkit, which is now handled by `NMS#getNativeProxyCommandSender` (more on that below)
    - `wrapCommandSender` removed
    - Wrapping the sender no longer necessary for `getBrigadierSourceFromCommandSender`
    - `CommandAPIVelocity` now does nothing in these methods :P

- `CommandAPIExecutor` reworked
  - `ExecutorType` moved to platform modules (so Velocity can no longer try to define sender types that don't exist)
  - Detecting whether a certain executor can be run by the given class delegated to `BukkitTypedExecutor` and `VelocityTypedExecutor`
  - Priority of executors now depends on order of calling the `executes` methods (i.e the priority listed here https://commandapi.jorel.dev/9.4.1/normalexecutors.html#multiple-command-executor-implementations no longer applies)
  - Normal executors are no longer ignored if resulting executors exist (not sure if this was a bug or intended)

- Tweaked `ExecutionInfo`
  - Added `CommandContext<Source> cmdCtx` to `ExecutionInfo`
    - This allows passing the information needed for a `NATIVE` executor on Bukkit to create a `NativeProxyCommandSender`
      - Uses `NMS#getNativeProxyCommandSender`, which was adapted from the removed `getSenderForCommand` method
      - Note: conflicts with #478, though should be easily resolved
    - Note: Velocity can define the `CommandSource` object, though Bukkit has it as `?`. This makes it hard for non DSL Kotlin to infer the type parameters for some reason, which is annoying because you now need to specify the type parameter. I don't know if there's a better way to handle that.
    - TODO: Make sure depdendents can still use `ExecutionInfo` without including Brigadier as a dependency
  - `ExecutionInfo` is now a record (`BukkitExecutionInfo` and `VelocityExecutionInfo` removed)

- Simplified `dev.jorel.commandapi.executors` package
  - Platform and sender specific executor classes (e.g. `PlayerCommandExecutor` and `EntityResultingExecutionInfo`) removed
  - Just the functional interfaces `NormalExecutorInfo`, `NormalExecutor`, `ResultingExecutorInfo`, and `ResultingExecutor` now
  - `BukkitExecutable` and `VelocityExecutable` link different command sender classes as type parameters to the `ExecutorType` enum values

TODO:
- Add executor tests to `dev/dev` to ensure same behavior
  - Especially for `PROXY` and `NATIVE` senders
  - Especially for non-DSL Kotlin to see how its lamdba type inference works
- Update documentation
willkroboth added a commit that referenced this pull request May 17, 2024
Note: This commit makes this branch backwards incompatible, especially for non-DSL Kotlin code (see `commandapi-documentation-code/.../Examples.kt`). "Standard" Java and DSL API code usage looks the same, but I'm pretty sure plugins will need to recompile due to changes to the `FunctionalInterface`s. There are also some smaller public API changes, mentioned below.

Notable changes:
- Removed `AbstractCommandSender` and all its implemenations (i.e. the `dev.jorel.commandapi.commandsenders` package is completely gone)
  - Logic in `CommandAPIHandler#generateBrigadierRequirements` for checking if a sender satisfies a `CommandPermission` was moved to `CommandAPIPlatform#getPermissionCheck`
    - Previously, methods in `AbstractCommandSender` provided access to `hasPermission` and `isOp`. `CommandAPIBukkit` and `CommandAPIVelocity` now handle these definitions.
    -  `CommandPermission.TRUE()` and `CommandPermission.FALSE()` added for computing short circuits when combining permissions and requirements
  - `PreviewInfo` now has the generic parameter `Player` rather than an `AbstractPlayer`
    - Backwards-incompatible (`(Player) player.getSource()` becomes `player`)
    - Generic parameter propogates to `PreviewableFunction` and `Previewable`
  - `CommandAPIPlatform` methods for convert Brigadier Source to CommandSender simplified
    - `getSenderForCommand` removed
      - just pass `CommandContext#getSource` into `getCommandSenderFromCommandSource`
      - `forceNative` parameter was only used on Bukkit, which is now handled by `NMS#getNativeProxyCommandSender` (more on that below)
    - `wrapCommandSender` removed
    - Wrapping the sender no longer necessary for `getBrigadierSourceFromCommandSender`
    - `CommandAPIVelocity` now does nothing in these methods :P

- `CommandAPIExecutor` reworked
  - `ExecutorType` moved to platform modules (so Velocity can no longer try to define sender types that don't exist)
  - Detecting whether a certain executor can be run by the given class delegated to `BukkitTypedExecutor` and `VelocityTypedExecutor`
  - Priority of executors now depends on order of calling the `executes` methods (i.e the priority listed here https://commandapi.jorel.dev/9.4.1/normalexecutors.html#multiple-command-executor-implementations no longer applies)
  - Normal executors are no longer ignored if resulting executors exist (not sure if this was a bug or intended)

- Tweaked `ExecutionInfo`
  - Added `CommandContext<Source> cmdCtx` to `ExecutionInfo`
    - This allows passing the information needed for a `NATIVE` executor on Bukkit to create a `NativeProxyCommandSender`
      - Uses `NMS#getNativeProxyCommandSender`, which was adapted from the removed `getSenderForCommand` method
      - Note: conflicts with #478, though should be easily resolved
    - Note: Velocity can define the `CommandSource` object, though Bukkit has it as `?`. This makes it hard for non DSL Kotlin to infer the type parameters for some reason, which is annoying because you now need to specify the type parameter. I don't know if there's a better way to handle that.
    - TODO: Make sure depdendents can still use `ExecutionInfo` without including Brigadier as a dependency
  - `ExecutionInfo` is now a record (`BukkitExecutionInfo` and `VelocityExecutionInfo` removed)

- Simplified `dev.jorel.commandapi.executors` package
  - Platform and sender specific executor classes (e.g. `PlayerCommandExecutor` and `EntityResultingExecutionInfo`) removed
  - Just the functional interfaces `NormalExecutorInfo`, `NormalExecutor`, `ResultingExecutorInfo`, and `ResultingExecutor` now
  - `BukkitExecutable` and `VelocityExecutable` link different command sender classes as type parameters to the `ExecutorType` enum values

TODO:
- Add executor tests to `dev/dev` to ensure same behavior
  - Especially for `PROXY` and `NATIVE` senders
  - Especially for non-DSL Kotlin to see how its lamdba type inference works
- Update documentation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

CommandAPI's NativeProxyCommandSender cannot run vanilla commands
2 participants