001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.bukkit.player;
020
021import com.google.common.base.Charsets;
022import com.plotsquared.bukkit.util.BukkitUtil;
023import com.plotsquared.core.PlotSquared;
024import com.plotsquared.core.configuration.Settings;
025import com.plotsquared.core.events.TeleportCause;
026import com.plotsquared.core.location.Location;
027import com.plotsquared.core.permissions.Permission;
028import com.plotsquared.core.permissions.PermissionHandler;
029import com.plotsquared.core.player.ConsolePlayer;
030import com.plotsquared.core.player.PlotPlayer;
031import com.plotsquared.core.plot.PlotWeather;
032import com.plotsquared.core.plot.world.PlotAreaManager;
033import com.plotsquared.core.util.EventDispatcher;
034import com.plotsquared.core.util.MathMan;
035import com.sk89q.worldedit.bukkit.BukkitAdapter;
036import com.sk89q.worldedit.extension.platform.Actor;
037import com.sk89q.worldedit.world.item.ItemType;
038import com.sk89q.worldedit.world.item.ItemTypes;
039import io.papermc.lib.PaperLib;
040import net.kyori.adventure.audience.Audience;
041import org.bukkit.GameMode;
042import org.bukkit.Sound;
043import org.bukkit.WeatherType;
044import org.bukkit.entity.Player;
045import org.bukkit.event.Event;
046import org.bukkit.event.EventException;
047import org.bukkit.event.player.PlayerTeleportEvent;
048import org.bukkit.permissions.PermissionAttachmentInfo;
049import org.bukkit.plugin.RegisteredListener;
050import org.bukkit.potion.PotionEffectType;
051import org.checkerframework.checker.index.qual.NonNegative;
052import org.checkerframework.checker.nullness.qual.NonNull;
053
054import java.util.Arrays;
055import java.util.Set;
056import java.util.UUID;
057
058import static com.sk89q.worldedit.world.gamemode.GameModes.ADVENTURE;
059import static com.sk89q.worldedit.world.gamemode.GameModes.CREATIVE;
060import static com.sk89q.worldedit.world.gamemode.GameModes.SPECTATOR;
061import static com.sk89q.worldedit.world.gamemode.GameModes.SURVIVAL;
062
063public class BukkitPlayer extends PlotPlayer<Player> {
064
065    private static boolean CHECK_EFFECTIVE = true;
066    public final Player player;
067    private String name;
068
069    /**
070     * @param plotAreaManager   PlotAreaManager instance
071     * @param eventDispatcher   EventDispatcher instance
072     * @param player            Bukkit player instance
073     * @param permissionHandler PermissionHandler instance
074     *
075     * @deprecated Please do not use this method. Instead use {@link BukkitUtil#adapt(Player)}, as it caches player objects.
076     * This method will be made private in a future release.
077     */
078    @Deprecated(forRemoval = true, since = "6.10.9")
079    public BukkitPlayer(
080            final @NonNull PlotAreaManager plotAreaManager, final @NonNull EventDispatcher eventDispatcher,
081            final @NonNull Player player, final @NonNull PermissionHandler permissionHandler
082    ) {
083        this(plotAreaManager, eventDispatcher, player, false, permissionHandler);
084    }
085
086    /**
087     * @param plotAreaManager   PlotAreaManager instance
088     * @param eventDispatcher   EventDispatcher instance
089     * @param player            Bukkit player instance
090     * @param permissionHandler PermissionHandler instance
091     *
092     * @deprecated Please do not use this method. Instead use {@link BukkitUtil#adapt(Player)}, as it caches player objects.
093     * This method will be made private in a future release.
094     */
095    @Deprecated(forRemoval = true, since = "6.10.9")
096    public BukkitPlayer(
097            final @NonNull PlotAreaManager plotAreaManager, final @NonNull
098            EventDispatcher eventDispatcher, final @NonNull Player player,
099            final boolean realPlayer,
100            final @NonNull PermissionHandler permissionHandler
101    ) {
102        super(plotAreaManager, eventDispatcher, permissionHandler);
103        this.player = player;
104        this.setupPermissionProfile();
105        if (realPlayer) {
106            super.populatePersistentMetaMap();
107        }
108    }
109
110    @Override
111    public Actor toActor() {
112        return BukkitAdapter.adapt(player);
113    }
114
115    @Override
116    public Player getPlatformPlayer() {
117        return this.player;
118    }
119
120    @NonNull
121    @Override
122    public UUID getUUID() {
123        if (Settings.UUID.OFFLINE) {
124            if (Settings.UUID.FORCE_LOWERCASE) {
125                return UUID.nameUUIDFromBytes(("OfflinePlayer:" +
126                        getName().toLowerCase()).getBytes(Charsets.UTF_8));
127            } else {
128                return UUID.nameUUIDFromBytes(("OfflinePlayer:" +
129                        getName()).getBytes(Charsets.UTF_8));
130            }
131        }
132        return player.getUniqueId();
133    }
134
135    @Override
136    @NonNegative
137    public long getLastPlayed() {
138        return this.player.getLastSeen();
139    }
140
141    @Override
142    public boolean canTeleport(final @NonNull Location location) {
143        final org.bukkit.Location to = BukkitUtil.adapt(location);
144        final org.bukkit.Location from = player.getLocation();
145        PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to);
146        callEvent(event);
147        if (event.isCancelled() || !event.getTo().equals(to)) {
148            return false;
149        }
150        event = new PlayerTeleportEvent(player, to, from);
151        callEvent(event);
152        return true;
153    }
154
155    private void callEvent(final @NonNull Event event) {
156        final RegisteredListener[] listeners = event.getHandlers().getRegisteredListeners();
157        for (final RegisteredListener listener : listeners) {
158            if (listener.getPlugin().getName().equals(PlotSquared.platform().pluginName())) {
159                continue;
160            }
161            try {
162                listener.callEvent(event);
163            } catch (final EventException e) {
164                e.printStackTrace();
165            }
166        }
167    }
168
169    @SuppressWarnings("StringSplitter")
170    @Override
171    @NonNegative
172    public int hasPermissionRange(
173            final @NonNull String stub,
174            @NonNegative final int range
175    ) {
176        if (hasPermission(Permission.PERMISSION_ADMIN.toString())) {
177            return Integer.MAX_VALUE;
178        }
179        final String[] nodes = stub.split("\\.");
180        final StringBuilder n = new StringBuilder();
181        for (int i = 0; i < (nodes.length - 1); i++) {
182            n.append(nodes[i]).append(".");
183            if (!stub.equals(n + Permission.PERMISSION_STAR.toString())) {
184                if (hasPermission(n + Permission.PERMISSION_STAR.toString())) {
185                    return Integer.MAX_VALUE;
186                }
187            }
188        }
189        if (hasPermission(stub + ".*")) {
190            return Integer.MAX_VALUE;
191        }
192        int max = 0;
193        if (CHECK_EFFECTIVE) {
194            boolean hasAny = false;
195            String stubPlus = stub + ".";
196            final Set<PermissionAttachmentInfo> effective = player.getEffectivePermissions();
197            if (!effective.isEmpty()) {
198                for (PermissionAttachmentInfo attach : effective) {
199                    String permStr = attach.getPermission();
200                    if (permStr.startsWith(stubPlus)) {
201                        hasAny = true;
202                        String end = permStr.substring(stubPlus.length());
203                        if (MathMan.isInteger(end)) {
204                            int val = Integer.parseInt(end);
205                            if (val > range) {
206                                return val;
207                            }
208                            if (val > max) {
209                                max = val;
210                            }
211                        }
212                    }
213                }
214                if (hasAny) {
215                    return max;
216                }
217                // Workaround
218                for (PermissionAttachmentInfo attach : effective) {
219                    String permStr = attach.getPermission();
220                    if (permStr.startsWith("plots.") && !permStr.equals("plots.use")) {
221                        return max;
222                    }
223                }
224                CHECK_EFFECTIVE = false;
225            }
226        }
227        for (int i = range; i > 0; i--) {
228            if (hasPermission(stub + "." + i)) {
229                return i;
230            }
231        }
232        return max;
233    }
234
235    @Override
236    public void teleport(final @NonNull Location location, final @NonNull TeleportCause cause) {
237        if (Math.abs(location.getX()) >= 30000000 || Math.abs(location.getZ()) >= 30000000) {
238            return;
239        }
240        final org.bukkit.Location bukkitLocation =
241                new org.bukkit.Location(BukkitUtil.getWorld(location.getWorldName()), location.getX() + 0.5,
242                        location.getY(), location.getZ() + 0.5, location.getYaw(), location.getPitch()
243                );
244        PaperLib.teleportAsync(player, bukkitLocation, getTeleportCause(cause));
245    }
246
247    @Override
248    public String getName() {
249        if (this.name == null) {
250            this.name = this.player.getName();
251        }
252        return this.name;
253    }
254
255    @Override
256    public void setCompassTarget(Location location) {
257        this.player.setCompassTarget(
258                new org.bukkit.Location(BukkitUtil.getWorld(location.getWorldName()), location.getX(),
259                        location.getY(), location.getZ()
260                ));
261    }
262
263    @Override
264    public Location getLocationFull() {
265        return BukkitUtil.adaptComplete(this.player.getLocation());
266    }
267
268    @Override
269    public void setWeather(final @NonNull PlotWeather weather) {
270        switch (weather) {
271            case CLEAR -> this.player.setPlayerWeather(WeatherType.CLEAR);
272            case RAIN -> this.player.setPlayerWeather(WeatherType.DOWNFALL);
273            case WORLD -> this.player.resetPlayerWeather();
274            default -> {
275                //do nothing as this is PlotWeather.OFF
276            }
277        }
278    }
279
280    @Override
281    public com.sk89q.worldedit.world.gamemode.GameMode getGameMode() {
282        return switch (this.player.getGameMode()) {
283            case ADVENTURE -> ADVENTURE;
284            case CREATIVE -> CREATIVE;
285            case SPECTATOR -> SPECTATOR;
286            default -> SURVIVAL;
287        };
288    }
289
290    @Override
291    public void setGameMode(final com.sk89q.worldedit.world.gamemode.GameMode gameMode) {
292        if (ADVENTURE.equals(gameMode)) {
293            this.player.setGameMode(GameMode.ADVENTURE);
294        } else if (CREATIVE.equals(gameMode)) {
295            this.player.setGameMode(GameMode.CREATIVE);
296        } else if (SPECTATOR.equals(gameMode)) {
297            this.player.setGameMode(GameMode.SPECTATOR);
298        } else {
299            this.player.setGameMode(GameMode.SURVIVAL);
300        }
301    }
302
303    @Override
304    public void setTime(final long time) {
305        if (time != Long.MAX_VALUE) {
306            this.player.setPlayerTime(time, false);
307        } else {
308            this.player.resetPlayerTime();
309        }
310    }
311
312    @Override
313    public boolean getFlight() {
314        return player.getAllowFlight();
315    }
316
317    @Override
318    public void setFlight(boolean fly) {
319        this.player.setAllowFlight(fly);
320    }
321
322    @Override
323    public void playMusic(final @NonNull Location location, final @NonNull ItemType id) {
324        if (id == ItemTypes.AIR) {
325            // Let's just stop all the discs because why not?
326            for (final Sound sound : Arrays.stream(Sound.values())
327                    .filter(sound -> sound.name().contains("DISC")).toList()) {
328                player.stopSound(sound);
329            }
330            // this.player.playEffect(BukkitUtil.getLocation(location), Effect.RECORD_PLAY, Material.AIR);
331        } else {
332            // this.player.playEffect(BukkitUtil.getLocation(location), Effect.RECORD_PLAY, id.to(Material.class));
333            this.player.playSound(BukkitUtil.adapt(location),
334                    Sound.valueOf(BukkitAdapter.adapt(id).name()), Float.MAX_VALUE, 1f
335            );
336        }
337    }
338
339    @SuppressWarnings("deprecation") // Needed for Spigot compatibility
340    @Override
341    public void kick(final String message) {
342        this.player.kickPlayer(message);
343    }
344
345    @Override
346    public void stopSpectating() {
347        if (getGameMode() == SPECTATOR) {
348            this.player.setSpectatorTarget(null);
349        }
350    }
351
352    @Override
353    public boolean isBanned() {
354        return this.player.isBanned();
355    }
356
357    @Override
358    public @NonNull Audience getAudience() {
359        return BukkitUtil.BUKKIT_AUDIENCES.player(this.player);
360    }
361
362    @Override
363    public void removeEffect(@NonNull String name) {
364        PotionEffectType type = PotionEffectType.getByName(name);
365        if (type != null) {
366            player.removePotionEffect(type);
367        }
368    }
369
370    @Override
371    public boolean canSee(final PlotPlayer<?> other) {
372        if (other instanceof ConsolePlayer) {
373            return true;
374        } else {
375            return this.player.canSee(((BukkitPlayer) other).getPlatformPlayer());
376        }
377    }
378
379    /**
380     * Convert from PlotSquared's {@link TeleportCause} to Bukkit's {@link PlayerTeleportEvent.TeleportCause}
381     *
382     * @param cause PlotSquared teleport cause to convert
383     * @return Bukkit's equivalent teleport cause
384     */
385    public PlayerTeleportEvent.TeleportCause getTeleportCause(final @NonNull TeleportCause cause) {
386        if (TeleportCause.CauseSets.COMMAND.contains(cause)) {
387            return PlayerTeleportEvent.TeleportCause.COMMAND;
388        } else if (cause == TeleportCause.UNKNOWN) {
389            return PlayerTeleportEvent.TeleportCause.UNKNOWN;
390        }
391        return PlayerTeleportEvent.TeleportCause.PLUGIN;
392    }
393
394}