diff --git a/api/src/main/java/org/allaymc/api/entity/component/EntityPlayerBaseComponent.java b/api/src/main/java/org/allaymc/api/entity/component/EntityPlayerBaseComponent.java index 40c67140d..c44389739 100644 --- a/api/src/main/java/org/allaymc/api/entity/component/EntityPlayerBaseComponent.java +++ b/api/src/main/java/org/allaymc/api/entity/component/EntityPlayerBaseComponent.java @@ -233,23 +233,12 @@ default boolean isActualPlayer() { void setFlying(boolean flying); /** - * Checks if the player is allowed to fly based on the current game mode and permissions. - * + * Checks if the player is allowed to fly based on the current game mode and abilities. * * @return {@code true} if the player can fly, {@code false} otherwise */ default boolean canFly() { - var gameMode = getGameMode(); - return switch (gameMode) { - case SPECTATOR -> true; - case CREATIVE -> hasPermission(Permissions.ABILITY_FLY_CREATIVE) != Tristate.FALSE; - case SURVIVAL -> hasPermission(Permissions.ABILITY_FLY_SURVIVAL) == Tristate.TRUE; - case ADVENTURE -> hasPermission(Permissions.ABILITY_FLY_ADVENTURE) == Tristate.TRUE; - }; + return isActualPlayer() ? getController().canFly() : false; } /** diff --git a/api/src/main/java/org/allaymc/api/eventbus/event/server/PlayerAbilitiesUpdateEvent.java b/api/src/main/java/org/allaymc/api/eventbus/event/server/PlayerAbilitiesUpdateEvent.java new file mode 100644 index 000000000..17d25f235 --- /dev/null +++ b/api/src/main/java/org/allaymc/api/eventbus/event/server/PlayerAbilitiesUpdateEvent.java @@ -0,0 +1,35 @@ +package org.allaymc.api.eventbus.event.server; + +import lombok.Getter; +import org.allaymc.api.annotation.CallerThread; +import org.allaymc.api.annotation.ThreadType; +import org.allaymc.api.player.Player; +import org.allaymc.api.player.PlayerAbility; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +/** + * Called after a player's abilities are changed. + * + * @author zernix2077 + */ +@Getter +@CallerThread(ThreadType.WORLD) +public class PlayerAbilitiesUpdateEvent extends ServerPlayerEvent { + protected final Set previous; + protected final Set current; + + public PlayerAbilitiesUpdateEvent(Player player, Set previous, Set current) { + super(player); + this.previous = copyAbilities(previous); + this.current = copyAbilities(current); + } + + protected static Set copyAbilities(Set abilities) { + return abilities.isEmpty() + ? Collections.unmodifiableSet(EnumSet.noneOf(PlayerAbility.class)) + : Collections.unmodifiableSet(EnumSet.copyOf(abilities)); + } +} diff --git a/api/src/main/java/org/allaymc/api/eventbus/event/server/PlayerAbilitiesUpdateRequestEvent.java b/api/src/main/java/org/allaymc/api/eventbus/event/server/PlayerAbilitiesUpdateRequestEvent.java new file mode 100644 index 000000000..32388e6fd --- /dev/null +++ b/api/src/main/java/org/allaymc/api/eventbus/event/server/PlayerAbilitiesUpdateRequestEvent.java @@ -0,0 +1,32 @@ +package org.allaymc.api.eventbus.event.server; + +import lombok.Getter; +import org.allaymc.api.annotation.CallerThread; +import org.allaymc.api.annotation.ThreadType; +import org.allaymc.api.eventbus.event.CancellableEvent; +import org.allaymc.api.player.Player; +import org.allaymc.api.player.PlayerAbility; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +/** + * Called before an operator's requested ability update is applied to another player. + * + * @author zernix2077 + */ +@Getter +@CallerThread(ThreadType.WORLD) +public class PlayerAbilitiesUpdateRequestEvent extends ServerPlayerEvent implements CancellableEvent { + protected final Player target; + protected final Set abilities; + + public PlayerAbilitiesUpdateRequestEvent(Player player, Player target, Set abilities) { + super(player); + this.target = target; + this.abilities = abilities.isEmpty() + ? Collections.unmodifiableSet(EnumSet.noneOf(PlayerAbility.class)) + : Collections.unmodifiableSet(EnumSet.copyOf(abilities)); + } +} diff --git a/api/src/main/java/org/allaymc/api/permission/OpPermissionCalculator.java b/api/src/main/java/org/allaymc/api/permission/OpPermissionCalculator.java index 5d57531bd..654058e52 100644 --- a/api/src/main/java/org/allaymc/api/permission/OpPermissionCalculator.java +++ b/api/src/main/java/org/allaymc/api/permission/OpPermissionCalculator.java @@ -25,21 +25,9 @@ public record OpPermissionCalculator(Player player) implements PermissionCalcula Permissions.COMMAND_VERSION )); - /** - * Permissions that should return UNDEFINED even for operators. - */ - public static final Set OP_UNDEFINED_PERMISSIONS = new HashSet<>(Set.of( - Permissions.ABILITY_FLY_SURVIVAL, - Permissions.ABILITY_FLY_CREATIVE, - Permissions.ABILITY_FLY_ADVENTURE - )); - @Override public Tristate calculatePermission(String permission) { if (Server.getInstance().getPlayerManager().isOperator(player)) { - if (OP_UNDEFINED_PERMISSIONS.contains(permission)) { - return Tristate.UNDEFINED; - } return Tristate.TRUE; } diff --git a/api/src/main/java/org/allaymc/api/permission/Permissions.java b/api/src/main/java/org/allaymc/api/permission/Permissions.java index ba3637cd0..7eeb3d68a 100644 --- a/api/src/main/java/org/allaymc/api/permission/Permissions.java +++ b/api/src/main/java/org/allaymc/api/permission/Permissions.java @@ -9,18 +9,6 @@ public interface Permissions { /* Ability */ - /** - * Permission to fly in survival mode. Only TRUE allows flying. - */ - String ABILITY_FLY_SURVIVAL = "ability.fly.survival"; - /** - * Permission to fly in creative mode. FALSE denies, otherwise allowed. - */ - String ABILITY_FLY_CREATIVE = "ability.fly.creative"; - /** - * Permission to fly in adventure mode. Only TRUE allows flying. - */ - String ABILITY_FLY_ADVENTURE = "ability.fly.adventure"; /** * The permission to chat. */ @@ -253,4 +241,4 @@ public interface Permissions { * The permission to use /xp command. */ String COMMAND_XP = "allay.command.xp"; -} \ No newline at end of file +} diff --git a/api/src/main/java/org/allaymc/api/player/GameMode.java b/api/src/main/java/org/allaymc/api/player/GameMode.java index 09ce9aee2..94860e0a1 100644 --- a/api/src/main/java/org/allaymc/api/player/GameMode.java +++ b/api/src/main/java/org/allaymc/api/player/GameMode.java @@ -2,8 +2,10 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.EnumSet; import org.allaymc.api.message.MayContainTrKey; import org.allaymc.api.message.TrKeys; +import org.jetbrains.annotations.UnmodifiableView; /** * GameMode represents a game mode that may be assigned to a player. Upon joining the world, players will be @@ -19,23 +21,23 @@ public enum GameMode { * SURVIVAL is the survival game mode: Players with this game mode have limited supplies and can break * blocks after taking some time. */ - SURVIVAL(TrKeys.MC_GAMEMODE_SURVIVAL), + SURVIVAL(TrKeys.MC_GAMEMODE_SURVIVAL, EnumSet.noneOf(PlayerAbility.class)), /** * CREATIVE represents the creative game mode: Players with this game mode have infinite blocks and * items and can break blocks instantly. Players with creative mode can also fly. */ - CREATIVE(TrKeys.MC_GAMEMODE_CREATIVE), + CREATIVE(TrKeys.MC_GAMEMODE_CREATIVE, EnumSet.of(PlayerAbility.MAY_FLY, PlayerAbility.INFINITE_BLOCK)), /** * ADVENTURE represents the adventure game mode: Players with this game mode cannot edit the world * (placing or breaking blocks). */ - ADVENTURE(TrKeys.MC_GAMEMODE_ADVENTURE), + ADVENTURE(TrKeys.MC_GAMEMODE_ADVENTURE, EnumSet.noneOf(PlayerAbility.class)), /** * SPECTATOR represents the spectator game mode: Players with this game mode cannot interact with the * world and cannot be seen by other players. spectator players can fly, like creative mode, and can * move through blocks. */ - SPECTATOR(TrKeys.MC_GAMEMODE_SPECTATOR); + SPECTATOR(TrKeys.MC_GAMEMODE_SPECTATOR, EnumSet.of(PlayerAbility.MAY_FLY, PlayerAbility.FLYING, PlayerAbility.NO_CLIP)); private static final GameMode[] VALUES = values(); @@ -44,6 +46,11 @@ public enum GameMode { */ private final @MayContainTrKey String translationKey; + /** + * The default abilities associated with this game mode that should be tracked server-side. + */ + private final @UnmodifiableView EnumSet abilities; + /** * Looks up a game mode by the id passed. * diff --git a/api/src/main/java/org/allaymc/api/player/Player.java b/api/src/main/java/org/allaymc/api/player/Player.java index 2585c8aa9..e1316677f 100644 --- a/api/src/main/java/org/allaymc/api/player/Player.java +++ b/api/src/main/java/org/allaymc/api/player/Player.java @@ -21,6 +21,7 @@ import java.net.SocketAddress; import java.util.Collection; import java.util.List; +import java.util.Set; /** * Represents a player in the server. A {@link Player} basically 'control' an {@link EntityPlayer}. @@ -148,12 +149,213 @@ default boolean isDisconnected() { int getPing(); /** - * Views the specified player's permission. This will update the permission level shown in the player list and some + * Views the specified player's abilities. This will update the permission level shown in the player list and some * in-game permissions/properties like whether the player can fly, chat, and the player's (vertical) fly speed etc. * * @param player the player to view */ - void viewPlayerPermission(Player player); + void viewPlayerAbilities(Player player); + + /** + * Returns the player's current abilities. + * + * @return an immutable view of the current abilities + */ + @UnmodifiableView + Set getAbilities(); + + /** + * Checks whether the player has the given ability enabled. + * + * @param ability the ability to check + * @return {@code true} if the ability is enabled, {@code false} otherwise + */ + boolean hasAbility(PlayerAbility ability); + + /** + * Sets whether the given ability is enabled. + * + * @param ability the ability to change + * @param value {@code true} to enable the ability, {@code false} to disable it + */ + void setAbility(PlayerAbility ability, boolean value); + + /** + * Sets whether the given abilities are enabled. + * + * @param abilities the abilities to change + * @param value {@code true} to enable the ability, {@code false} to disable it + */ + void setAbilities(Set abilities, boolean value); + + /** + * Replaces the player's abilities with the provided set. + * + * @param abilities the new abilities + */ + void setAbilities(Set abilities); + + /** + * Enables the given ability. + * + * @param ability the ability to enable + */ + default void addAbility(PlayerAbility ability) { + setAbility(ability, true); + } + + /** + * Enables the given abilities. + * + * @param abilities the abilities to enable + */ + default void addAbilities(PlayerAbility... abilities) { + setAbilities(Set.of(abilities), true); + } + + /** + * Disables the given ability. + * + * @param ability the abilities to disable + */ + default void removeAbility(PlayerAbility ability) { + setAbility(ability, false); + } + + /** + * Disables the given abilities. + * + * @param abilities the abilities to disable + */ + default void removeAbilities(PlayerAbility... abilities) { + setAbilities(Set.of(abilities), false); + } + + /** + * Checks whether the player may currently place blocks. + *

+ * Always true for operators, always false for spectator, adventure modes and immutableWorld, + * actual ability value otherwise. + *

+ * Directly reflects client-side placement behavior. + * + * @return {@code true} if placing blocks is currently allowed, {@code false} otherwise + */ + boolean canPlaceBlocks(); + + /** + * Checks whether the player may currently break blocks. + *

+ * Always true for operators, always false for spectator, adventure modes and immutableWorld, + * actual ability value otherwise. + *

+ * Directly reflects client-side mining behavior. + * + * @return {@code true} if breaking blocks is currently allowed, {@code false} otherwise + */ + boolean canBreakBlocks(); + + /** + * Checks whether the player may currently interact with blocks. + *

+ * Always true for operators, always false for spectator mode and immutable world, actual ability + * value otherwise. + *

+ * Directly reflects client-side interaction behavior. + * + * @return {@code true} if block interaction is currently allowed, {@code false} otherwise + */ + boolean canInteractWithBlocks(); + + /** + * Checks whether the player may currently open containers. + *

+ * Always true for operators, always false for spectator mode and immutable world, actual ability + * value otherwise. + * + * @return {@code true} if opening containers is currently allowed, {@code false} otherwise + */ + boolean canOpenContainers(); + + /** + * Checks whether the player may currently attack other players. + *

+ * Always true for operators, always false for spectator mode, actual ability value otherwise. + * + * @return {@code true} if attacking players is currently allowed, {@code false} otherwise + */ + boolean canAttackPlayers(); + + /** + * Checks whether the player may currently attack mobs. + *

+ * Always true for operators, always false for spectator mode, actual ability value otherwise. + * + * @return {@code true} if attacking mobs is currently allowed, {@code false} otherwise + */ + boolean canAttackMobs(); + + /** + * Checks whether the player may currently fly. + *

+ * Always true if isAlwaysFlying and spectator mode, actual ability value otherwise. + * + * @return {@code true} if flying is currently allowed, {@code false} otherwise + */ + boolean canFly(); + + /** + * Checks whether placed blocks should not be consumed from inventory. + *

+ * Usually {@code true} in creative mode, but always reflects the actual ability value. + * + * @return {@code true} if placed blocks are not consumed, {@code false} otherwise + */ + boolean hasInfiniteBlock(); + + /** + * Checks whether the player is currently in no clip mode. + *

+ * Always true for spectator mode, actual ability value otherwise. + * + * @return {@code true} if player is in no clip mode, {@code false} otherwise. + */ + boolean isNoClip(); + + /** + * Checks whether the player is currently treated as having immutable world enabled. + *

+ * Always false for operators, always true for adventure and spectator modes, actual value + * otherwise. + * + * @return {@code true} if immutable world is currently active, {@code false} otherwise + */ + boolean isImmutableWorld(); + + /** + * Sets whether immutable world should be enabled for the player. + *

+ * Forces the client to treat the world as non-interactive, similar to adventure mode. + * + * @param immutableWorld {@code true} to enable immutable world, {@code false} to disable it + */ + void setImmutableWorld(boolean immutableWorld); + + /** + * Checks whether the player is currently forced to be always flying. + *

+ * Always true for spectator mode and no clip, actual value otherwise. + * + * @return {@code true} if always flying is currently active, {@code false} otherwise + */ + boolean isAlwaysFlying(); + + /** + * Sets whether always flying should be active for the player. + * + * @param alwaysFlying {@code true} to enable always flying, {@code false} to disable it + */ + void setAlwaysFlying(boolean alwaysFlying); /** * Views a player list change. The provided players will be added to the player list. diff --git a/api/src/main/java/org/allaymc/api/player/PlayerAbility.java b/api/src/main/java/org/allaymc/api/player/PlayerAbility.java new file mode 100644 index 000000000..9539c3da1 --- /dev/null +++ b/api/src/main/java/org/allaymc/api/player/PlayerAbility.java @@ -0,0 +1,69 @@ +package org.allaymc.api.player; + +/** + * Represents a player ability tracked by the server. + * + * @author zernix2077 + */ +public enum PlayerAbility { + /** + * Whether the player can place blocks. + *

+ * Directly affects client-side ability to place blocks. + */ + PLACE_BLOCK, + + /** + * Whether the player can break blocks. + *

+ * Directly affects client-side ability to break blocks. + */ + BREAK_BLOCK, + + /** + * Whether the player can interact with blocks, like doors and switches. + *

+ * Directly affects client-side ability to perform those interactions. + */ + INTERACT_BLOCK, + + /** + * Whether blocks are not consumed when placed down. + *

+ * Generally enabled for creative mode, but may also be enabled for any other gamemode, resulting + * in the same infinite block behavior client-side. + */ + INFINITE_BLOCK, + + /** + * Whether the player can open containers. + */ + OPEN_CONTAINER, + + /** + * Whether the player can attack other players. + */ + ATTACK_PLAYER, + + /** + * Whether the player can attack mobs. + */ + ATTACK_MOB, + + /** + * Whether the player is currently flying. + */ + FLYING, + + /** + * Whether the player may start flying. + */ + MAY_FLY, + + /** + * Whether the player can move through blocks. + *

+ * Directly affects client-side movement behavior. + */ + NO_CLIP, +} diff --git a/api/src/main/java/org/allaymc/api/player/PlayerData.java b/api/src/main/java/org/allaymc/api/player/PlayerData.java index 3975f3579..811529ec5 100644 --- a/api/src/main/java/org/allaymc/api/player/PlayerData.java +++ b/api/src/main/java/org/allaymc/api/player/PlayerData.java @@ -9,6 +9,9 @@ import org.allaymc.api.utils.identifier.Identifier; import org.allaymc.api.world.dimension.DimensionTypes; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtType; + +import java.util.EnumSet; import static org.allaymc.api.utils.AllayNBTUtils.writeVector2f; import static org.allaymc.api.utils.AllayNBTUtils.writeVector3f; @@ -28,6 +31,7 @@ public class PlayerData { protected static final String TAG_NBT = "NBT"; protected static final String TAG_WORLD = "World"; protected static final String TAG_DIMENSION = "Dimension"; + protected static final String TAG_ABILITIES = "Abilities"; // EntityPlayer's nbt, which can be generated through the method EntityPlayer#saveNBT() protected NbtMap nbt; @@ -38,6 +42,7 @@ public class PlayerData { // world and dimension the player is in. protected String world; protected String dimension; + protected EnumSet abilities; public static PlayerData save(Player player) { var entity = player.getControlledEntity(); @@ -50,6 +55,7 @@ public static PlayerData save(Player player) { .nbt(entity.saveNBT()) .world(entity.getWorld().getWorldData().getDisplayName()) .dimension(entity.getDimension().getDimensionType().getIdentifier().toString()) + .abilities(player.getAbilities().isEmpty() ? EnumSet.noneOf(PlayerAbility.class) : EnumSet.copyOf(player.getAbilities())) .build(); } @@ -84,6 +90,19 @@ public static PlayerData fromNBT(NbtMap nbt) { builder.nbt(nbt.getCompound(TAG_NBT)) .world(nbt.getString(TAG_WORLD)) .dimension(readDimension(nbt)); + + if (nbt.containsKey(TAG_ABILITIES)) { + var abilities = EnumSet.noneOf(PlayerAbility.class); + for (var abilityName : nbt.getList(TAG_ABILITIES, NbtType.STRING)) { + try { + abilities.add(PlayerAbility.valueOf(abilityName)); + } catch (IllegalArgumentException e) { + log.warn("Unknown stored player ability {}, ignoring it", abilityName); + } + } + builder.abilities(abilities); + } + return builder.build(); } @@ -97,6 +116,11 @@ public NbtMap toNBT() { .putCompound(TAG_NBT, nbt) .putString(TAG_WORLD, world) .putString(TAG_DIMENSION, dimension); + + if (abilities != null) { + builder.putList(TAG_ABILITIES, NbtType.STRING, abilities.stream().map(Enum::name).toList()); + } + return builder.build(); } diff --git a/server/src/main/java/org/allaymc/server/command/defaults/GameTestCommand.java b/server/src/main/java/org/allaymc/server/command/defaults/GameTestCommand.java index aae4e81b1..604b0393e 100644 --- a/server/src/main/java/org/allaymc/server/command/defaults/GameTestCommand.java +++ b/server/src/main/java/org/allaymc/server/command/defaults/GameTestCommand.java @@ -28,6 +28,7 @@ import org.allaymc.api.message.LangCode; import org.allaymc.api.message.TrKeys; import org.allaymc.api.player.HudElement; +import org.allaymc.api.player.PlayerAbility; import org.allaymc.api.registry.Registries; import org.allaymc.api.server.Server; import org.allaymc.api.utils.AllayStringUtils; @@ -838,6 +839,22 @@ public void prepareCommandTree(CommandTree tree) { return context.success(); }, SenderType.PLAYER) .root() + .key("setimmutableworld") + .bool("value") + .exec((context, sender) -> { + boolean value = context.getResult(1); + sender.getController().setImmutableWorld(value); + return context.success(); + }, SenderType.PLAYER) + .root() + .key("setnoblocksconsumption") + .bool("value") + .exec((context, sender) -> { + boolean value = context.getResult(1); + sender.getController().setAbility(PlayerAbility.INFINITE_BLOCK, value); + return context.success(); + }, SenderType.PLAYER) + .root() .key("attachprimitiveshape") .target("target") .optional() @@ -867,6 +884,5 @@ public void prepareCommandTree(CommandTree tree) { context.addOutput("Done."); return context.success(); }, SenderType.PLAYER); - } } diff --git a/server/src/main/java/org/allaymc/server/container/impl/BaseContainer.java b/server/src/main/java/org/allaymc/server/container/impl/BaseContainer.java index 10a91d876..c94e24abe 100644 --- a/server/src/main/java/org/allaymc/server/container/impl/BaseContainer.java +++ b/server/src/main/java/org/allaymc/server/container/impl/BaseContainer.java @@ -10,10 +10,12 @@ import org.allaymc.api.container.Container; import org.allaymc.api.container.ContainerType; import org.allaymc.api.container.ContainerViewer; +import org.allaymc.api.container.interfaces.BlockContainer; import org.allaymc.api.eventbus.event.container.ContainerCloseEvent; import org.allaymc.api.eventbus.event.container.ContainerOpenEvent; import org.allaymc.api.item.ItemStack; import org.allaymc.api.item.interfaces.ItemAirStack; +import org.allaymc.api.player.Player; import org.allaymc.api.utils.NBTIO; import org.cloudburstmc.nbt.NbtList; import org.cloudburstmc.nbt.NbtMap; @@ -89,6 +91,9 @@ public boolean addViewer(ContainerViewer viewer) { removeViewer(viewer); return addViewer(viewer); } + if (viewer instanceof Player player && this instanceof BlockContainer && !player.canOpenContainers()) { + return false; + } var event = new ContainerOpenEvent(viewer, this); if (!event.call()) { diff --git a/server/src/main/java/org/allaymc/server/container/impl/DoubleChestContainerImpl.java b/server/src/main/java/org/allaymc/server/container/impl/DoubleChestContainerImpl.java index 84fb4a51d..e4fe0e204 100644 --- a/server/src/main/java/org/allaymc/server/container/impl/DoubleChestContainerImpl.java +++ b/server/src/main/java/org/allaymc/server/container/impl/DoubleChestContainerImpl.java @@ -17,6 +17,7 @@ import org.allaymc.api.eventbus.event.container.ContainerOpenEvent; import org.allaymc.api.item.ItemStack; import org.allaymc.api.math.position.Position3ic; +import org.allaymc.api.player.Player; import org.cloudburstmc.nbt.NbtMap; import java.util.*; @@ -193,6 +194,9 @@ public boolean addViewer(ContainerViewer viewer) { removeViewer(viewer); return addViewer(viewer); } + if (viewer instanceof Player player && !player.canOpenContainers()) { + return false; + } var event = new ContainerOpenEvent(viewer, this); if (!event.call()) { diff --git a/server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerBaseComponentImpl.java b/server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerBaseComponentImpl.java index ced229422..a9f6be17f 100644 --- a/server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerBaseComponentImpl.java +++ b/server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerBaseComponentImpl.java @@ -24,6 +24,7 @@ import org.allaymc.api.message.TrContainer; import org.allaymc.api.player.GameMode; import org.allaymc.api.player.Player; +import org.allaymc.api.player.PlayerAbility; import org.allaymc.api.player.Skin; import org.allaymc.api.server.Server; import org.allaymc.api.utils.AllayNBTUtils; @@ -177,7 +178,7 @@ public void setController(Player controller) { @Override public void onPermissionChange() { if (isActualPlayer()) { - this.controller.viewPlayerPermission(this.controller); + this.controller.viewPlayerAbilities(this.controller); } } @@ -205,6 +206,7 @@ public void setGameMode(GameMode gameMode) { } gameMode = event.getNewGameMode(); + var oldGameMode = this.gameMode; this.gameMode = gameMode; this.manager.callEvent(new CPlayerGameModeChangeEvent(this.gameMode)); @@ -216,10 +218,16 @@ public void setGameMode(GameMode gameMode) { thisPlayer.setAirSupplyTicks(thisPlayer.getAirSupplyMaxTicks()); } + // Update abilities + if (isActualPlayer()) { + this.controller.setAbilities(oldGameMode.getAbilities(), false); + this.controller.setAbilities(gameMode.getAbilities(), true); + } + if (isActualPlayer()) { this.controller.viewPlayerGameMode(thisPlayer); // Send permission after game mode to make overriding client's state (e.g., mayfly) possible - this.controller.viewPlayerPermission(this.controller); + this.controller.viewPlayerAbilities(this.controller); } forEachViewers(viewer -> viewer.viewPlayerGameMode(thisPlayer)); } @@ -229,7 +237,7 @@ public void setFlying(boolean flying) { if (this.flying != flying) { this.flying = flying; if (isActualPlayer()) { - this.controller.viewPlayerPermission(this.controller); + this.controller.setAbility(PlayerAbility.FLYING, flying); } } } diff --git a/server/src/main/java/org/allaymc/server/item/component/ItemBaseComponentImpl.java b/server/src/main/java/org/allaymc/server/item/component/ItemBaseComponentImpl.java index edbdbef8e..d094991ad 100644 --- a/server/src/main/java/org/allaymc/server/item/component/ItemBaseComponentImpl.java +++ b/server/src/main/java/org/allaymc/server/item/component/ItemBaseComponentImpl.java @@ -384,7 +384,7 @@ protected void tryApplyBlockEntityNBT(Dimension dimension, Vector3ic placeBlockP } protected void tryConsumeItem(EntityPlayer player) { - if (player == null || player.getGameMode() != GameMode.CREATIVE) { + if (player == null || !player.getController().hasInfiniteBlock()) { thisItemStack.reduceCount(1); } } diff --git a/server/src/main/java/org/allaymc/server/network/processor/PacketProcessorHolder.java b/server/src/main/java/org/allaymc/server/network/processor/PacketProcessorHolder.java index 5ad099482..e97a466ad 100644 --- a/server/src/main/java/org/allaymc/server/network/processor/PacketProcessorHolder.java +++ b/server/src/main/java/org/allaymc/server/network/processor/PacketProcessorHolder.java @@ -148,6 +148,7 @@ private void registerInGamePacketProcessors() { registerProcessor(ClientState.IN_GAME, new BookEditPacketProcessor()); registerProcessor(ClientState.IN_GAME, new MapInfoRequestPacketProcessor()); registerProcessor(ClientState.IN_GAME, new RequestAbilityPacketProcessor()); + registerProcessor(ClientState.IN_GAME, new RequestPermissionsPacketProcessor()); registerProcessor(ClientState.IN_GAME, new NPCRequestPacketProcessor()); registerProcessor(ClientState.IN_GAME, new LecternUpdatePacketProcessor()); registerProcessor(ClientState.IN_GAME, new CommandBlockUpdatePacketProcessor()); diff --git a/server/src/main/java/org/allaymc/server/network/processor/ingame/InventoryTransactionPacketProcessor.java b/server/src/main/java/org/allaymc/server/network/processor/ingame/InventoryTransactionPacketProcessor.java index d32c01273..9a5f14283 100644 --- a/server/src/main/java/org/allaymc/server/network/processor/ingame/InventoryTransactionPacketProcessor.java +++ b/server/src/main/java/org/allaymc/server/network/processor/ingame/InventoryTransactionPacketProcessor.java @@ -7,6 +7,7 @@ import org.allaymc.api.container.ContainerTypes; import org.allaymc.api.entity.component.EntityLivingComponent; import org.allaymc.api.entity.damage.DamageContainer; +import org.allaymc.api.entity.interfaces.EntityPlayer; import org.allaymc.api.eventbus.event.player.*; import org.allaymc.api.player.Player; import org.allaymc.api.world.sound.AttackSound; @@ -85,6 +86,12 @@ public void handleSync(Player player, InventoryTransactionPacket packet, long re clickPos, blockFace ); + if (!player.canInteractWithBlocks()) { + player.viewBlockUpdate(clickBlockPos, 0, dimension.getBlockState(clickBlockPos)); + player.viewBlockUpdate(placeBlockPos, 0, dimension.getBlockState(placeBlockPos)); + break; + } + var event = new PlayerInteractBlockEvent(entity, interactInfo, PlayerInteractBlockEvent.Action.RIGHT_CLICK); if (!event.call()) { player.viewBlockUpdate(clickBlockPos, 0, dimension.getBlockState(clickBlockPos)); @@ -116,6 +123,11 @@ public void handleSync(Player player, InventoryTransactionPacket packet, long re break; } + if (!player.canPlaceBlocks()) { + player.viewBlockUpdate(placeBlockPos, 0, dimension.getBlockState(placeBlockPos)); + break; + } + if (!itemInHand.placeBlock(dimension, placeBlockPos, interactInfo)) { var blockStateReplaced = dimension.getBlockState(placeBlockPos); player.viewBlockUpdate(placeBlockPos, 0, blockStateReplaced); @@ -193,6 +205,13 @@ public void handleSync(Player player, InventoryTransactionPacket packet, long re if (!(target instanceof EntityLivingComponent damageable)) { return; } + if (target instanceof EntityPlayer) { + if (!player.canAttackPlayers()) { + return; + } + } else if (!player.canAttackMobs()) { + return; + } var damage = itemInHand.calculateAttackDamage(entity, target); if (damage == 0) { diff --git a/server/src/main/java/org/allaymc/server/network/processor/ingame/PlayerActionPacketProcessor.java b/server/src/main/java/org/allaymc/server/network/processor/ingame/PlayerActionPacketProcessor.java index 487bb8315..44839f813 100644 --- a/server/src/main/java/org/allaymc/server/network/processor/ingame/PlayerActionPacketProcessor.java +++ b/server/src/main/java/org/allaymc/server/network/processor/ingame/PlayerActionPacketProcessor.java @@ -22,6 +22,9 @@ public PacketSignal handleAsync(Player player, PlayerActionPacket packet, long r log.debug("Player {} tried to start item use on without stopping", player.getOriginName()); yield PacketSignal.HANDLED; } + if (!player.canInteractWithBlocks()) { + yield PacketSignal.HANDLED; + } entity.setUsingItemOnBlock(true); yield PacketSignal.HANDLED; diff --git a/server/src/main/java/org/allaymc/server/network/processor/ingame/PlayerAuthInputPacketProcessor.java b/server/src/main/java/org/allaymc/server/network/processor/ingame/PlayerAuthInputPacketProcessor.java index 2de07eb75..e8a8b82f5 100644 --- a/server/src/main/java/org/allaymc/server/network/processor/ingame/PlayerAuthInputPacketProcessor.java +++ b/server/src/main/java/org/allaymc/server/network/processor/ingame/PlayerAuthInputPacketProcessor.java @@ -34,6 +34,7 @@ import org.cloudburstmc.protocol.bedrock.packet.ItemStackRequestPacket; import org.cloudburstmc.protocol.bedrock.packet.PacketSignal; import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket; +import org.joml.Vector3d; import org.joml.Vector3i; import java.util.List; @@ -98,7 +99,7 @@ protected void handleBlockAction(Player player, List bloc switch (action.getAction()) { case START_BREAK -> { - if (isInvalidGameType(player)) { + if (isInvalidGameType(player) || !player.canBreakBlocks()) { continue; } @@ -106,7 +107,7 @@ protected void handleBlockAction(Player player, List bloc } case BLOCK_CONTINUE_DESTROY -> { // When a player switches to breaking another block halfway through breaking one - if (isInvalidGameType(player)) { + if (isInvalidGameType(player) || !player.canBreakBlocks()) { continue; } @@ -119,7 +120,9 @@ protected void handleBlockAction(Player player, List bloc startBreak(player, pos.getX(), pos.getY(), pos.getZ(), action.getFace()); } case BLOCK_PREDICT_DESTROY -> { - if (isInvalidGameType(player)) { + if (isInvalidGameType(player) || !player.canBreakBlocks()) { + var state = player.getControlledEntity().getLocation().dimension().getBlockState(new Vector3d(pos.getX(), pos.getY(), pos.getZ())); + player.viewBlockUpdate(new Vector3i(pos.getX(), pos.getY(), pos.getZ()), 0, state); continue; } @@ -348,10 +351,10 @@ protected void handleInputData(Player player, Set inputData entity.exhaust(entity.isSprinting() ? 0.2f : 0.05f); } case START_FLYING -> { - if (!entity.canFly()) { + if (!player.canFly()) { // Reset client-side flying state var controller = entity.getController(); - controller.viewPlayerPermission(controller); + controller.viewPlayerAbilities(controller); log.warn("Player {} tried to start flying without permission", player.getOriginName()); return; @@ -359,7 +362,15 @@ protected void handleInputData(Player player, Set inputData entity.setFlying(true); } - case STOP_FLYING -> entity.setFlying(false); + case STOP_FLYING -> { + entity.setFlying(player.isAlwaysFlying()); + if (player.isAlwaysFlying()) { + entity.setFlying(true); + player.viewPlayerAbilities(player); + } else { + entity.setFlying(false); + } + } case MISSED_SWING -> { var event = new PlayerPunchAirEvent(entity); if (event.call()) { diff --git a/server/src/main/java/org/allaymc/server/network/processor/ingame/RequestAbilityPacketProcessor.java b/server/src/main/java/org/allaymc/server/network/processor/ingame/RequestAbilityPacketProcessor.java index 5ffd26c80..fa346d93e 100644 --- a/server/src/main/java/org/allaymc/server/network/processor/ingame/RequestAbilityPacketProcessor.java +++ b/server/src/main/java/org/allaymc/server/network/processor/ingame/RequestAbilityPacketProcessor.java @@ -2,6 +2,7 @@ import org.allaymc.api.player.Player; import org.allaymc.server.network.processor.PacketProcessor; +import org.cloudburstmc.protocol.bedrock.data.Ability; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacketType; import org.cloudburstmc.protocol.bedrock.packet.PacketSignal; import org.cloudburstmc.protocol.bedrock.packet.RequestAbilityPacket; @@ -12,8 +13,29 @@ public class RequestAbilityPacketProcessor extends PacketProcessor { @Override - public PacketSignal handleAsync(Player player, RequestAbilityPacket packet, long receiveTime) { - return PacketSignal.HANDLED; + public void handleSync(Player player, RequestAbilityPacket packet, long receiveTime) { + if (packet.getAbility() != Ability.FLYING || packet.getType() != Ability.Type.BOOLEAN) { + return; + } + + var entity = player.getControlledEntity(); + if (packet.isBoolValue()) { + if (!player.canFly()) { + player.viewPlayerAbilities(player); + return; + } + + entity.setFlying(true); + return; + } + + if (player.isAlwaysFlying()) { + entity.setFlying(true); + player.viewPlayerAbilities(player); + return; + } + + entity.setFlying(false); } @Override diff --git a/server/src/main/java/org/allaymc/server/network/processor/ingame/RequestPermissionsPacketProcessor.java b/server/src/main/java/org/allaymc/server/network/processor/ingame/RequestPermissionsPacketProcessor.java new file mode 100644 index 000000000..892ba8cf5 --- /dev/null +++ b/server/src/main/java/org/allaymc/server/network/processor/ingame/RequestPermissionsPacketProcessor.java @@ -0,0 +1,85 @@ +package org.allaymc.server.network.processor.ingame; + +import org.allaymc.api.eventbus.event.server.PlayerAbilitiesUpdateRequestEvent; +import org.allaymc.api.player.Player; +import org.allaymc.api.player.PlayerAbility; +import org.allaymc.api.server.Server; +import org.allaymc.server.network.processor.PacketProcessor; +import org.cloudburstmc.protocol.bedrock.data.Ability; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacketType; +import org.cloudburstmc.protocol.bedrock.packet.RequestPermissionsPacket; + +import java.util.EnumSet; +import java.util.Set; + +/** + * @author zernix2077 + */ +public class RequestPermissionsPacketProcessor extends PacketProcessor { + protected static final PlayerAbility[] CONTROLLABLE_ABILITIES = { + PlayerAbility.PLACE_BLOCK, + PlayerAbility.BREAK_BLOCK, + PlayerAbility.INTERACT_BLOCK, + PlayerAbility.OPEN_CONTAINER, + PlayerAbility.ATTACK_PLAYER, + PlayerAbility.ATTACK_MOB + }; + + @Override + public void handleSync(Player player, RequestPermissionsPacket packet, long receiveTime) { + if (!Server.getInstance().getPlayerManager().isOperator(player)) { + return; + } + + Player target = null; + for (var onlinePlayer : Server.getInstance().getPlayerManager().getPlayers().values()) { + var entity = onlinePlayer.getControlledEntity(); + if (entity != null && entity.getUniqueId().getLeastSignificantBits() == packet.getUniqueEntityId()) { + target = onlinePlayer; + break; + } + } + if (target == null) { + return; + } + + var updated = EnumSet.noneOf(PlayerAbility.class); + updated.addAll(target.getAbilities()); + var requestedPermissions = packet.getCustomPermissions(); + for (var ability : CONTROLLABLE_ABILITIES) { + if (hasNetworkAbility(requestedPermissions, ability)) { + updated.add(ability); + } else { + updated.remove(ability); + } + } + + if (new PlayerAbilitiesUpdateRequestEvent(player, target, updated).call()) { + target.setAbilities(updated); + } + } + + protected boolean hasNetworkAbility(Set abilities, PlayerAbility ability) { + if (abilities == null) { + return false; + } + + return abilities.contains(switch (ability) { + case PLACE_BLOCK -> Ability.BUILD; + case BREAK_BLOCK -> Ability.MINE; + case INTERACT_BLOCK -> Ability.DOORS_AND_SWITCHES; + case OPEN_CONTAINER -> Ability.OPEN_CONTAINERS; + case ATTACK_PLAYER -> Ability.ATTACK_PLAYERS; + case ATTACK_MOB -> Ability.ATTACK_MOBS; + case FLYING -> Ability.FLYING; + case MAY_FLY -> Ability.MAY_FLY; + case INFINITE_BLOCK -> Ability.INSTABUILD; + case NO_CLIP -> Ability.NO_CLIP; + }); + } + + @Override + public BedrockPacketType getPacketType() { + return BedrockPacketType.REQUEST_PERMISSIONS; + } +} diff --git a/server/src/main/java/org/allaymc/server/player/AllayPlayer.java b/server/src/main/java/org/allaymc/server/player/AllayPlayer.java index 3074fc667..5800cb48c 100644 --- a/server/src/main/java/org/allaymc/server/player/AllayPlayer.java +++ b/server/src/main/java/org/allaymc/server/player/AllayPlayer.java @@ -40,6 +40,7 @@ import org.allaymc.api.entity.interfaces.*; import org.allaymc.api.entity.type.EntityTypes; import org.allaymc.api.eventbus.event.server.PlayerDisconnectEvent; +import org.allaymc.api.eventbus.event.server.PlayerAbilitiesUpdateEvent; import org.allaymc.api.eventbus.event.server.PlayerLoginEvent; import org.allaymc.api.eventbus.event.server.PlayerSpawnEvent; import org.allaymc.api.form.type.CustomForm; @@ -218,6 +219,12 @@ public class AllayPlayer implements Player { // Fog protected final List fogStack = new ArrayList<>(); + // Abilities + protected final EnumSet abilities; + protected boolean shouldSendAbilities; + protected boolean immutableWorld; + protected boolean alwaysFlying; + // NetEase @Getter @Setter @@ -251,6 +258,9 @@ public AllayPlayer(BedrockServerSession session, AllayNetworkInterface sourceInt // Hud this.hiddenHudElements = EnumSet.noneOf(HudElement.class); + + // Abilities + this.abilities = EnumSet.noneOf(PlayerAbility.class); } protected static LevelChunkPacket createSubChunkLevelChunkPacket(AllayUnsafeChunk chunk, ChunkCache cache, UUID playerId, int cacheGen) { @@ -346,6 +356,13 @@ public void tick(long currentTick) { this.sendHudElements(); this.shouldSendHudElements = false; } + + if (this.shouldSendAbilities && + this.controlledEntity != null && + getClientState().ordinal() >= ClientState.SPAWNED.ordinal()) { + broadcastPlayerAbilities(); + this.shouldSendAbilities = false; + } } public void handlePacketSync(BedrockPacket packet, long receiveTime) { @@ -384,8 +401,12 @@ protected void onFullyJoin() { viewContainerContents(this.controlledEntity.getContainer(ContainerTypes.INVENTORY)); viewContainerContents(this.controlledEntity.getContainer(ContainerTypes.OFFHAND)); viewContainerContents(this.controlledEntity.getContainer(ContainerTypes.ARMOR)); - viewPlayerPermission(this); + viewPlayerAbilities(this); viewPlayerListChange(playerManager.getPlayers().values(), true); + playerManager.getPlayers().values().stream() + .filter(player -> player != this && player.getControlledEntity() != null) + .forEach(this::viewPlayerAbilities); + broadcastPlayerAbilities(); sendSpeed(this.speed); sendExperienceLevel(this.controlledEntity.getExperienceLevel()); @@ -491,17 +512,11 @@ public void removeEntity(Entity entity) { public void viewPlayerGameMode(EntityPlayer player) { var gameMode = player.getGameMode(); if (this.controlledEntity == player) { - var packet1 = new SetPlayerGameTypePacket(); - packet1.setGamemode(toNetwork(player.getGameMode()).ordinal()); - sendPacket(packet1); - - var packet2 = new UpdateAdventureSettingsPacket(); - packet2.setNoPvM(gameMode == GameMode.SPECTATOR); - packet2.setNoMvP(gameMode == GameMode.SPECTATOR); - packet2.setShowNameTags(gameMode != GameMode.SPECTATOR); - packet2.setImmutableWorld(gameMode == GameMode.SPECTATOR); - packet2.setAutoJump(true); - sendPacket(packet2); + var packet = new SetPlayerGameTypePacket(); + packet.setGamemode(toNetwork(player.getGameMode()).ordinal()); + sendPacket(packet); + + sendAdventureSettings(); } else { var packet = new UpdatePlayerGameTypePacket(); packet.setGameType(toNetwork(player.getGameMode())); @@ -2957,7 +2972,7 @@ public int getPing() { } @Override - public void viewPlayerPermission(Player player) { + public void viewPlayerAbilities(Player player) { var packet = new UpdateAbilitiesPacket(); var entity = Preconditions.checkNotNull(player.getControlledEntity()); @@ -2966,12 +2981,12 @@ public void viewPlayerPermission(Player player) { // If this player does not have specific command permissions, the command description won't even be sent to the client packet.setCommandPermission(entity.hasPermission(Permissions.ABILITY_OPERATOR_COMMAND_QUICK_BAR).asBoolean() ? CommandPermission.GAME_DIRECTORS : CommandPermission.ANY); // PlayerPermissions is the permission level of the player as it shows up in the player list built up using the PlayerList packet - packet.setPlayerPermission(calculatePlayerPermission(entity)); + packet.setPlayerPermission(calculatePlayerPermission(player)); var layer = new AbilityLayer(); layer.setLayerType(AbilityLayer.Type.BASE); layer.getAbilitiesSet().addAll(Arrays.asList(Ability.values())); - layer.getAbilityValues().addAll(calculateAbilities(entity)); + layer.getAbilityValues().addAll(calculateAbilities(player)); // NOTICE: this shouldn't be changed layer.setWalkSpeed((float) DEFAULT_SPEED.calculate()); layer.setFlySpeed((float) player.getFlySpeed().calculate()); @@ -2981,56 +2996,341 @@ public void viewPlayerPermission(Player player) { sendPacket(packet); if (player == this) { + sendAdventureSettings(); this.shouldSendCommands = true; + this.shouldSendAbilities = false; } } - protected EnumSet calculateAbilities(EntityPlayer player) { - var gameMode = player.getGameMode(); + protected void broadcastPlayerAbilities() { + if (this.controlledEntity == null) { + return; + } + viewPlayerAbilities(this); + this.controlledEntity.forEachViewers(viewer -> { + if (viewer instanceof Player playerViewer && playerViewer != this) { + playerViewer.viewPlayerAbilities(this); + } + }); + } + + protected EnumSet calculateAbilities(Player player) { var abilities = EnumSet.noneOf(Ability.class); - abilities.add(Ability.TELEPORT); - abilities.add(Ability.WALK_SPEED); - abilities.add(Ability.FLY_SPEED); - abilities.add(Ability.VERTICAL_FLY_SPEED); - - if (gameMode != GameMode.SPECTATOR) { - abilities.add(Ability.BUILD); - abilities.add(Ability.MINE); - abilities.add(Ability.DOORS_AND_SWITCHES); - abilities.add(Ability.OPEN_CONTAINERS); - abilities.add(Ability.ATTACK_PLAYERS); - abilities.add(Ability.ATTACK_MOBS); - } else { - abilities.add(Ability.NO_CLIP); - abilities.add(Ability.FLYING); + for (var ability : player.getAbilities()) { + abilities.add(toNetworkAbility(ability)); } - if (gameMode == GameMode.CREATIVE) { - abilities.add(Ability.INSTABUILD); + if (player.getControlledEntity() != null && player.getControlledEntity().hasPermission(Permissions.ABILITY_OPERATOR_COMMAND_QUICK_BAR).asBoolean()) { + abilities.add(Ability.OPERATOR_COMMANDS); } - if (player.canFly()) { - abilities.add(Ability.MAY_FLY); + if (!player.canFly()) { + abilities.remove(Ability.MAY_FLY); + abilities.remove(Ability.FLYING); } - if (player.isFlying() && abilities.contains(Ability.MAY_FLY)) { + if (player.isAlwaysFlying()) { + abilities.add(Ability.MAY_FLY); abilities.add(Ability.FLYING); } - if (player.isActualPlayer() && Server.getInstance().getPlayerManager().isOperator(player.getController())) { - abilities.add(Ability.OPERATOR_COMMANDS); + return abilities; + } + + protected PlayerPermission calculatePlayerPermission(Player player) { + var build = player.canPlaceBlocks(); + var mine = player.canBreakBlocks(); + var doorsAndSwitches = player.canInteractWithBlocks(); + var openContainers = player.canOpenContainers(); + var attackPlayers = player.canAttackPlayers(); + var attackMobs = player.canAttackMobs(); + + if ( + Server.getInstance().getPlayerManager().isOperator(player) && + build && mine && doorsAndSwitches && openContainers && attackPlayers && attackMobs + ) { + return PlayerPermission.OPERATOR; + } + + if (build && mine && doorsAndSwitches && openContainers && attackPlayers && attackMobs) { + return PlayerPermission.MEMBER; + } + + if (!build && !mine && !doorsAndSwitches && !openContainers && !attackPlayers && !attackMobs) { + return PlayerPermission.VISITOR; } + return PlayerPermission.CUSTOM; + } + + protected void sendAdventureSettings() { + if (this.controlledEntity == null || getClientState().ordinal() < ClientState.SPAWNED.ordinal()) { + return; + } + + var packet = new UpdateAdventureSettingsPacket(); + packet.setNoPvM(!canAttackMobs()); + packet.setNoMvP(!canAttackMobs()); + packet.setShowNameTags(this.controlledEntity.getGameMode() != GameMode.SPECTATOR); + packet.setImmutableWorld(isImmutableWorld()); + packet.setAutoJump(true); + sendPacket(packet); + } + + protected static EnumSet createBaseAbilitySet() { + return EnumSet.noneOf(PlayerAbility.class); + } + + protected static EnumSet abilitiesFromPermission(PlayerPermission permission) { + var abilities = createBaseAbilitySet(); + switch (permission) { + case OPERATOR -> abilities.addAll(EnumSet.of( + PlayerAbility.PLACE_BLOCK, + PlayerAbility.BREAK_BLOCK, + PlayerAbility.INTERACT_BLOCK, + PlayerAbility.OPEN_CONTAINER, + PlayerAbility.ATTACK_PLAYER, + PlayerAbility.ATTACK_MOB + )); + case MEMBER -> abilities.addAll(EnumSet.of( + PlayerAbility.PLACE_BLOCK, + PlayerAbility.BREAK_BLOCK, + PlayerAbility.INTERACT_BLOCK, + PlayerAbility.OPEN_CONTAINER, + PlayerAbility.ATTACK_PLAYER, + PlayerAbility.ATTACK_MOB + )); + case VISITOR, CUSTOM -> { + // Keep only the common non-permission abilities. + } + } return abilities; } - protected PlayerPermission calculatePlayerPermission(EntityPlayer player) { - if (player.isActualPlayer() && Server.getInstance().getPlayerManager().isOperator(player.getController())) { - return PlayerPermission.OPERATOR; + protected Ability toNetworkAbility(PlayerAbility ability) { + return switch (ability) { + case PLACE_BLOCK -> Ability.BUILD; + case BREAK_BLOCK -> Ability.MINE; + case INTERACT_BLOCK -> Ability.DOORS_AND_SWITCHES; + case OPEN_CONTAINER -> Ability.OPEN_CONTAINERS; + case ATTACK_PLAYER -> Ability.ATTACK_PLAYERS; + case ATTACK_MOB -> Ability.ATTACK_MOBS; + case FLYING -> Ability.FLYING; + case MAY_FLY -> Ability.MAY_FLY; + case INFINITE_BLOCK -> Ability.INSTABUILD; + case NO_CLIP -> Ability.NO_CLIP; + }; + } + + protected void scheduleAbilitiesUpdate(Set previous) { + if (this.controlledEntity != null) { + new PlayerAbilitiesUpdateEvent(this, previous, this.abilities).call(); + } + this.shouldSendAbilities = true; + } + + @Override + public @UnmodifiableView Set getAbilities() { + return Collections.unmodifiableSet(abilities); + } + + @Override + public boolean hasAbility(PlayerAbility ability) { + return this.abilities.contains(ability); + } + + @Override + public void setAbility(PlayerAbility ability, boolean value) { + if (this.abilities.contains(ability) == value) { + return; + } + + var wasAlwaysFlying = isAlwaysFlying(); + var couldFly = canFly(); + + var snap = EnumSet.copyOf(this.abilities); + if (value) { + this.abilities.add(ability); + } else { + this.abilities.remove(ability); + } + scheduleAbilitiesUpdate(snap); + + syncFlyingState(wasAlwaysFlying, couldFly); + } + + @Override + public void setAbilities(Set abilities, boolean value) { + if ((value && this.abilities.containsAll(abilities)) || (!value && Collections.disjoint(this.abilities, abilities))) { + return; + } + + var wasAlwaysFlying = isAlwaysFlying(); + var couldFly = canFly(); + + var snap = EnumSet.copyOf(this.abilities); + if (value) { + this.abilities.addAll(abilities); + } else { + this.abilities.removeAll(abilities); + } + scheduleAbilitiesUpdate(snap); + + syncFlyingState(wasAlwaysFlying, couldFly); + } + + @Override + public void setAbilities(Set abilities) { + if (this.abilities.equals(abilities)) { + return; + } + + var wasAlwaysFlying = isAlwaysFlying(); + var couldFly = canFly(); + + var snap = EnumSet.copyOf(this.abilities); + this.abilities.clear(); + this.abilities.addAll(abilities); + scheduleAbilitiesUpdate(snap); + + syncFlyingState(wasAlwaysFlying, couldFly); + } + + @Override + public boolean canPlaceBlocks() { + if (isImmutableWorld()) { + return false; + } + if (this.controlledEntity != null && this.controlledEntity.getGameMode() == GameMode.ADVENTURE) { + return false; + } + if (Server.getInstance().getPlayerManager().isOperator(this)) { + return true; + } + return hasAbility(PlayerAbility.PLACE_BLOCK); + } + + @Override + public boolean canBreakBlocks() { + if (isImmutableWorld()) { + return false; + } + if (this.controlledEntity != null && this.controlledEntity.getGameMode() == GameMode.ADVENTURE) { + return false; + } + if (Server.getInstance().getPlayerManager().isOperator(this)) { + return true; + } + return hasAbility(PlayerAbility.BREAK_BLOCK); + } + + @Override + public boolean canInteractWithBlocks() { + if (isImmutableWorld()) { + return false; + } + if (this.controlledEntity != null && this.controlledEntity.getGameMode() == GameMode.SPECTATOR) { + return false; + } + if (Server.getInstance().getPlayerManager().isOperator(this)) { + return true; + } + return hasAbility(PlayerAbility.INTERACT_BLOCK); + } + + @Override + public boolean canOpenContainers() { + if (this.controlledEntity != null && this.controlledEntity.getGameMode() == GameMode.SPECTATOR) { + return false; + } + if (Server.getInstance().getPlayerManager().isOperator(this)) { + return true; + } + return hasAbility(PlayerAbility.OPEN_CONTAINER); + } + + @Override + public boolean canAttackPlayers() { + if (this.controlledEntity != null && this.controlledEntity.getGameMode() == GameMode.SPECTATOR) { + return false; + } + if (Server.getInstance().getPlayerManager().isOperator(this)) { + return true; + } + return hasAbility(PlayerAbility.ATTACK_PLAYER); + } + + @Override + public boolean canAttackMobs() { + if (this.controlledEntity != null && this.controlledEntity.getGameMode() == GameMode.SPECTATOR) { + return false; + } + if (Server.getInstance().getPlayerManager().isOperator(this)) { + return true; + } + return hasAbility(PlayerAbility.ATTACK_MOB); + } + + @Override + public boolean canFly() { + return this.controlledEntity != null && (isAlwaysFlying() || hasAbility(PlayerAbility.MAY_FLY)); + } + + @Override + public boolean hasInfiniteBlock() { + return hasAbility(PlayerAbility.INFINITE_BLOCK); + } + + @Override + public boolean isNoClip() { + return this.controlledEntity != null && (hasAbility(PlayerAbility.NO_CLIP) || this.controlledEntity.getGameMode() == GameMode.SPECTATOR); + } + + @Override + public boolean isImmutableWorld() { + if (Server.getInstance().getPlayerManager().isOperator(this)) { + return false; + } + + if (this.immutableWorld) { + return true; + } + + if (this.controlledEntity != null) { + return this.controlledEntity.getGameMode() == GameMode.SPECTATOR || this.controlledEntity.getGameMode() == GameMode.ADVENTURE; } - return PlayerPermission.MEMBER; + return false; + } + + @Override + public void setImmutableWorld(boolean immutableWorld) { + if (this.immutableWorld != immutableWorld) { + this.immutableWorld = immutableWorld; + this.shouldSendAbilities = true; + } + } + + @Override + public boolean isAlwaysFlying() { + return this.alwaysFlying || hasAbility(PlayerAbility.NO_CLIP) || (this.controlledEntity != null && this.controlledEntity.getGameMode() == GameMode.SPECTATOR); + } + + @Override + public void setAlwaysFlying(boolean alwaysFlying) { + var wasAlwaysFlying = this.alwaysFlying; + this.alwaysFlying = alwaysFlying; + this.shouldSendAbilities = true; + syncFlyingState(wasAlwaysFlying, canFly()); + } + + protected void syncFlyingState(boolean wasAlwaysFlying, boolean couldFly) { + if (!wasAlwaysFlying && isAlwaysFlying()) { + this.controlledEntity.setFlying(true); + } else if (couldFly && !canFly()) { + this.controlledEntity.setFlying(false); + } } @Override @@ -3071,7 +3371,7 @@ protected void sendSpeed(Speed speed) { public void setFlySpeed(Speed flySpeed) { if (!this.flySpeed.equals(flySpeed)) { this.flySpeed = flySpeed; - viewPlayerPermission(this); + viewPlayerAbilities(this); } } @@ -3079,7 +3379,7 @@ public void setFlySpeed(Speed flySpeed) { public void setVerticalFlySpeed(Speed verticalFlySpeed) { if (!this.verticalFlySpeed.equals(verticalFlySpeed)) { this.verticalFlySpeed = verticalFlySpeed; - viewPlayerPermission(this); + viewPlayerAbilities(this); } } @@ -3157,6 +3457,17 @@ public void spawnEntityPlayer() { currentPos = readVector3f(playerData.getNbt(), "Pos"); } + var storedAbilities = playerData.getAbilities(); + if (storedAbilities == null) { + var permissionName = AllayServer.getSettings().genericSettings().defaultPermission().toUpperCase(); + this.abilities.addAll(abilitiesFromPermission(PlayerPermission.valueOf(permissionName))); + + var gameMode = GameMode.from(playerData.getNbt().getInt("PlayerGameMode", NetworkHelper.toNetwork(dimension.getWorld().getWorldData().getGameMode()).ordinal())); + this.abilities.addAll(gameMode.getAbilities()); + } else { + this.abilities.addAll(storedAbilities); + } + this.controlledEntity = EntityTypes.PLAYER.createEntity(); this.controlledEntity.setSkin(this.loginData.getSkin()); this.controlledEntity.setDisplayName(loginData.getXname()); @@ -3237,7 +3548,7 @@ protected void startGame(World spawnWorld, PlayerData playerData, Dimension dime packet.getGamerules().addAll(NetworkHelper.toNetwork(spawnWorld.getWorldData().getGameRules().getGameRules())); packet.setUniqueEntityId(this.controlledEntity.getUniqueId().getLeastSignificantBits()); packet.setRuntimeEntityId(this.controlledEntity.getRuntimeId()); - packet.setPlayerGameType(GameType.from(playerData.getNbt().getInt("GameType", NetworkHelper.toNetwork(spawnWorld.getWorldData().getGameMode()).ordinal()))); + packet.setPlayerGameType(GameType.from(playerData.getNbt().getInt("PlayerGameMode", NetworkHelper.toNetwork(spawnWorld.getWorldData().getGameMode()).ordinal()))); var loc = this.controlledEntity.getLocation(); var worldSpawn = spawnWorld.getWorldData().getSpawnPoint(); packet.setDefaultSpawn(Vector3i.from(worldSpawn.x(), worldSpawn.y(), worldSpawn.z())); diff --git a/server/src/main/java/org/allaymc/server/player/AllayPlayerManager.java b/server/src/main/java/org/allaymc/server/player/AllayPlayerManager.java index f8c809c0d..b58eea8fa 100644 --- a/server/src/main/java/org/allaymc/server/player/AllayPlayerManager.java +++ b/server/src/main/java/org/allaymc/server/player/AllayPlayerManager.java @@ -265,7 +265,7 @@ public void setOperator(String uuidOrName, boolean value) { players.values().stream() .filter(p -> p.getLoginData().getUuid().toString().equals(uuidOrName) || p.getOriginName().equals(uuidOrName)) .findFirst() - .ifPresent(player -> player.viewPlayerPermission(player)); + .ifPresent(player -> this.players.values().forEach(viewer -> viewer.viewPlayerAbilities(player))); } public void startNetworkInterfaces() {