- add default handlers for neoforge

- dont register blank permissions
- handle neoforge's picky hasPermission
- register all Geyser non-command permissions as unset permissions
This commit is contained in:
onebeastchris 2024-04-21 04:34:26 +02:00
parent 27a086ec72
commit 7456ed3c1f
17 changed files with 112 additions and 45 deletions

View File

@ -32,8 +32,7 @@ import org.geysermc.geyser.api.util.TriState;
* Fired by anything that wishes to gather permission nodes and defaults.
* <p>
* This event is not guaranteed to be fired, as certain Geyser platforms do not have a native permission system.
* It can be expected to fire on Geyser-Spigot, Geyser-NeoForge and Geyser-Standalone. Note: NeoForge allows registering permissions,
* but does so without registering default values.
* It can be expected to fire on Geyser-Spigot, Geyser-NeoForge and Geyser-Standalone.
* It may still be fired on other platforms due to a 3rd party.
*/
public interface GeyserRegisterPermissionsEvent extends Event {

View File

@ -28,6 +28,7 @@ package org.geysermc.geyser.platform.neoforge;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.world.entity.player.Player;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.EventPriority;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.neoforge.common.NeoForge;
@ -35,7 +36,6 @@ import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.command.CommandSourceConverter;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
@ -60,7 +60,7 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap {
NeoForge.EVENT_BUS.addListener(this::onPlayerJoin);
GeyserNeoForgePermissionHandler permissionHandler = new GeyserNeoForgePermissionHandler();
NeoForge.EVENT_BUS.addListener(permissionHandler::onPermissionGather);
NeoForge.EVENT_BUS.addListener(EventPriority.HIGHEST, permissionHandler::onPermissionGather);
this.onGeyserInitialize();
@ -75,7 +75,7 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap {
ExecutionCoordinator.simpleCoordinator(),
sourceConverter
);
this.setCommandRegistry(new CommandRegistry(GeyserImpl.getInstance(), cloud));
this.setCommandRegistry(new GeyserNeoForgeCommandRegistry(GeyserImpl.getInstance(), cloud));
}
private void onServerStarted(ServerStartedEvent event) {

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.platform.neoforge;
import com.google.common.util.concurrent.UncheckedExecutionException;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.incendo.cloud.CommandManager;
public class GeyserNeoForgeCommandRegistry extends CommandRegistry {
public GeyserNeoForgeCommandRegistry(GeyserImpl geyser, CommandManager<GeyserCommandSource> cloud) {
super(geyser, cloud);
}
// todo yeet once cloud enforced method contract here:
// https://github.com/Incendo/cloud/blob/master/cloud-core/src/main/java/org/incendo/cloud/CommandManager.java#L441-L449
@Override
public boolean hasPermission(GeyserCommandSource source, String permission) {
try {
return super.hasPermission(source, permission);
} catch (UncheckedExecutionException e) {
return false;
}
}
}

View File

@ -30,9 +30,9 @@ import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContextKe
import net.neoforged.neoforge.server.permission.nodes.PermissionNode;
import net.neoforged.neoforge.server.permission.nodes.PermissionType;
import net.neoforged.neoforge.server.permission.nodes.PermissionTypes;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
import org.geysermc.geyser.api.util.TriState;
import java.lang.reflect.Constructor;
@ -57,22 +57,27 @@ public class GeyserNeoForgePermissionHandler {
}
public void onPermissionGather(PermissionGatherEvent.Nodes event) {
this.registerNode(Constants.UPDATE_PERMISSION, event);
GeyserImpl.getInstance().eventBus().fire((GeyserRegisterPermissionsEvent) (permission, defaultValue) -> this.registerNode(permission, event));
GeyserImpl.getInstance().eventBus().fire(
(GeyserRegisterPermissionsEvent) (permission, defaultValue) -> {
if (permission.isBlank()) {
return;
}
this.registerNode(permission, defaultValue, event);
}
);
}
private void registerNode(String node, PermissionGatherEvent.Nodes event) {
PermissionNode<Boolean> permissionNode = this.createNode(node);
private void registerNode(String node, TriState permissionDefault, PermissionGatherEvent.Nodes event) {
PermissionNode<Boolean> permissionNode = this.createNode(node, permissionDefault);
// NeoForge likes to crash if you try and register a duplicate node
if (!event.getNodes().contains(permissionNode)) {
if (event.getNodes().stream().noneMatch(eventNode -> eventNode.getNodeName().equals(node))) {
event.addNodes(permissionNode);
}
}
@SuppressWarnings("unchecked")
private PermissionNode<Boolean> createNode(String node) {
private PermissionNode<Boolean> createNode(String node, TriState permissionDefault) {
// The typical constructors in PermissionNode require a
// mod id, which means our permission nodes end up becoming
// geyser_neoforge.<node> instead of just <node>. We work around
@ -82,7 +87,16 @@ public class GeyserNeoForgePermissionHandler {
return (PermissionNode<Boolean>) PERMISSION_NODE_CONSTRUCTOR.newInstance(
node,
PermissionTypes.BOOLEAN,
(PermissionNode.PermissionResolver<Boolean>) (player, playerUUID, context) -> false,
(PermissionNode.PermissionResolver<Boolean>) (player, playerUUID, context) -> switch (permissionDefault) {
case TRUE -> true;
case FALSE -> false;
case NOT_SET -> {
if (player != null) {
yield player.createCommandSourceStack().hasPermission(player.server.getOperatorUserPermissionLevel());
}
yield false;
}
},
new PermissionDynamicContextKey[0]
);
} catch (Exception e) {

View File

@ -88,7 +88,7 @@ public class ModCommandSource implements GeyserCommandSource {
// Unlike other bootstraps; we delegate to cloud here too:
// On NeoForge; we'd have to keep track of all PermissionNodes - cloud already does that
// For Fabric, we won't need to include the Fabric Permissions API anymore - cloud already does that too :p
return GeyserImpl.getInstance().commandRegistry().cloud().hasPermission(this, permission);
return GeyserImpl.getInstance().commandRegistry().hasPermission(this, permission);
}
@Override

View File

@ -41,7 +41,6 @@ import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.adapters.spigot.SpigotAdapters;
@ -277,14 +276,13 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserLogger.debug("Using default world manager.");
}
// Register permissions so they appear in, for example, LuckPerms' UI
// Re-registering permissions without removing it throws an error
PluginManager pluginManager = Bukkit.getPluginManager();
// todo: this can probably always be run regardless if geyser has been initialized once or not, since we are removing the permission
geyser.eventBus().fire((GeyserRegisterPermissionsEvent) (permission, def) -> {
if (permission.isBlank()) {
return;
}
PermissionDefault permissionDefault = switch (def) {
case TRUE -> PermissionDefault.TRUE;
case FALSE -> PermissionDefault.FALSE;
@ -302,9 +300,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
pluginManager.addPermission(new Permission(permission, permissionDefault));
});
pluginManager.addPermission(new Permission(Constants.UPDATE_PERMISSION,
"Whether update notifications can be seen", PermissionDefault.OP));
// Events cannot be unregistered - re-registering results in duplicate firings
GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(geyser, this.geyserWorldManager);
pluginManager.registerEvents(blockPlaceListener, this);

View File

@ -38,6 +38,8 @@ public final class Constants {
public static final String GEYSER_DOWNLOAD_LOCATION = "https://geysermc.org/download";
public static final String UPDATE_PERMISSION = "geyser.update";
public static final String SERVER_SETTINGS_PERMISSION = "geyser.settings.server";
public static final String SETTINGS_GAMERULES_PERMISSION = "geyser.settings.gamerules";
static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json";

View File

@ -28,6 +28,7 @@ package org.geysermc.geyser.command;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.AllArgsConstructor;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.api.command.Command;
@ -112,7 +113,7 @@ public class CommandRegistry {
new GeyserExceptionHandler<>(NoSuchCommandException.class, (src, e) -> src.sendLocaleString("geyser.command.not_found")),
new GeyserExceptionHandler<>(ArgumentParseException.class, (src, e) -> src.sendLocaleString("geyser.command.invalid_argument", e.getCause().getMessage())),
new GeyserExceptionHandler<>(CommandExecutionException.class, (src, e) -> handleUnexpectedThrowable(src, e.getCause())),
new GeyserExceptionHandler<>(RuntimeException.class, (src, e) -> handleUnexpectedThrowable(src, e.getCause()))
new GeyserExceptionHandler<>(Throwable.class, (src, e) -> handleUnexpectedThrowable(src, e.getCause()))
);
for (GeyserExceptionHandler<?> handler : exceptionHandlers) {
handler.register(cloud);
@ -195,6 +196,10 @@ public class CommandRegistry {
register(command, this.extensionCommands.computeIfAbsent(extension, e -> new HashMap<>()));
}
public boolean hasPermission(GeyserCommandSource source, String permission) {
return cloud.hasPermission(source, permission);
}
private void register(GeyserCommand command, Map<String, Command> commands) {
command.register(cloud);
@ -218,6 +223,11 @@ public class CommandRegistry {
for (Map.Entry<String, TriState> permission : permissionDefaults.entrySet()) {
event.register(permission.getKey(), permission.getValue());
}
// Register other various Geyser permissions
event.register(Constants.UPDATE_PERMISSION, TriState.NOT_SET);
event.register(Constants.SERVER_SETTINGS_PERMISSION, TriState.NOT_SET);
event.register(Constants.SETTINGS_GAMERULES_PERMISSION, TriState.NOT_SET);
}
/**
@ -277,7 +287,7 @@ public class CommandRegistry {
}
@AllArgsConstructor
private static class GeyserExceptionHandler<E extends Exception> implements ExceptionHandler<GeyserCommandSource, E> {
private static class GeyserExceptionHandler<E extends Throwable> implements ExceptionHandler<GeyserCommandSource, E> {
final Class<E> type;
final BiConsumer<GeyserCommandSource, E> handler;

View File

@ -89,6 +89,9 @@ public abstract class GeyserCommand implements org.geysermc.geyser.api.command.C
if (name.isBlank()) {
throw new IllegalArgumentException("Command cannot be null or blank!");
}
if (permission.isBlank()) {
permissionDefault = null;
}
this.name = name;
this.description = description;

View File

@ -42,6 +42,9 @@ public class GeyserPermission implements PredicatePermission<GeyserCommandSource
private final CommandManager<GeyserCommandSource> manager;
public Result check(GeyserCommandSource source) {
if (permission.isBlank()) {
return Result.ALLOWED;
}
if (bedrockOnly) {
if (source.connection() == null) {
return Result.NOT_BEDROCK;

View File

@ -86,6 +86,9 @@ public class StandaloneCloudCommandManager extends CommandManager<GeyserCommandS
*/
public void gatherPermissions() {
geyser.getEventBus().fire((GeyserRegisterPermissionsEvent) (permission, def) -> {
if (permission.isBlank()) {
return;
}
if (def == TriState.TRUE) {
basePermissions.add(permission);
}

View File

@ -27,7 +27,6 @@ package org.geysermc.geyser.extension.command;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.command.Command;
import org.geysermc.geyser.api.command.CommandExecutor;
import org.geysermc.geyser.api.command.CommandSource;
@ -111,12 +110,6 @@ public abstract class GeyserExtensionCommand extends GeyserCommand {
@Override
public Builder<T> permission(@NonNull String permission) {
this.permission = Objects.requireNonNull(permission, "command permission");
if (!permission.contains(".") && !permission.isBlank()) {
String newPermission = extension.description().id() + "." + permission;
GeyserImpl.getInstance().getLogger().error("Extension " + extension.name() + " tried to register an invalid permission (" + permission + ")." +
"Changing it to " + newPermission + "!");
this.permission = newPermission;
}
return this;
}
@ -124,12 +117,6 @@ public abstract class GeyserExtensionCommand extends GeyserCommand {
public Builder<T> permission(@NonNull String permission, @NonNull TriState defaultValue) {
this.permission = Objects.requireNonNull(permission, "command permission");
this.permissionDefault = Objects.requireNonNull(defaultValue, "command permission defaultValue");
if (!permission.contains(".") && !permission.isBlank()) {
String newPermission = extension.description().id() + "." + permission;
GeyserImpl.getInstance().getLogger().error("Extension " + extension.name() + " tried to register an invalid permission (" + permission + ")." +
"Changing it to " + newPermission + "!");
this.permission = newPermission;
}
return this;
}

View File

@ -65,8 +65,6 @@ import io.netty.channel.Channel;
import io.netty.channel.EventLoop;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
@ -1463,7 +1461,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
public boolean hasPermission(String permission) {
// for Geyser-Standalone, standalone's permission system will handle it.
// for server platforms, the session will be mapped to a server command sender, and the server's api will be used.
return geyser.commandRegistry().cloud().hasPermission(this, permission);
return geyser.commandRegistry().hasPermission(this, permission);
}
/**

View File

@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import org.cloudburstmc.protocol.bedrock.packet.SetDefaultGameTypePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@ -41,7 +42,7 @@ public class BedrockSetDefaultGameTypeTranslator extends PacketTranslator<SetDef
*/
@Override
public void translate(GeyserSession session, SetDefaultGameTypePacket packet) {
if (session.getOpPermissionLevel() >= 2 && session.hasPermission("geyser.settings.server")) {
if (session.getOpPermissionLevel() >= 2 && session.hasPermission(Constants.SERVER_SETTINGS_PERMISSION)) {
session.getGeyser().getWorldManager().setDefaultGameMode(session, GameMode.byId(packet.getGamemode()));
}
// Stop the client from updating their own Gamemode without telling the server

View File

@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@ -39,7 +40,7 @@ public class BedrockSetDifficultyTranslator extends PacketTranslator<SetDifficul
*/
@Override
public void translate(GeyserSession session, SetDifficultyPacket packet) {
if (session.getOpPermissionLevel() >= 2 && session.hasPermission("geyser.settings.server")) {
if (session.getOpPermissionLevel() >= 2 && session.hasPermission(Constants.SERVER_SETTINGS_PERMISSION)) {
if (packet.getDifficulty() != session.getWorldCache().getDifficulty().ordinal()) {
session.getGeyser().getWorldManager().setDifficulty(session, Difficulty.from(packet.getDifficulty()));
}

View File

@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@ -45,7 +46,7 @@ public class BedrockSetPlayerGameTypeTranslator extends PacketTranslator<SetPlay
@Override
public void translate(GeyserSession session, SetPlayerGameTypePacket packet) {
// yes, if you are OP
if (session.getOpPermissionLevel() >= 2 && session.hasPermission("geyser.settings.server")) {
if (session.getOpPermissionLevel() >= 2 && session.hasPermission(Constants.SERVER_SETTINGS_PERMISSION)) {
if (packet.getGamemode() != session.getGameMode().ordinal()) {
// Bedrock has more Gamemodes than Java, leading to cases 5 (for "default") and 6 (for "spectator") being sent
// https://github.com/CloudburstMC/Protocol/blob/3.0/bedrock-codec/src/main/java/org/cloudburstmc/protocol/bedrock/data/GameType.java

View File

@ -27,6 +27,7 @@ package org.geysermc.geyser.util;
import org.geysermc.cumulus.component.DropdownComponent;
import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.level.GameRule;
import org.geysermc.geyser.level.WorldManager;
@ -79,7 +80,7 @@ public class SettingsUtils {
}
}
boolean showGamerules = session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.gamerules");
boolean showGamerules = session.getOpPermissionLevel() >= 2 || session.hasPermission(Constants.SETTINGS_GAMERULES_PERMISSION);
if (showGamerules) {
builder.label("geyser.settings.title.game_rules")
.translator(MinecraftLocale::getLocaleString); // we need translate gamerules next