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.plotsquared.core.util.WorldUtil;
036import com.sk89q.worldedit.bukkit.BukkitAdapter;
037import com.sk89q.worldedit.extension.platform.Actor;
038import com.sk89q.worldedit.world.item.ItemType;
039import com.sk89q.worldedit.world.item.ItemTypes;
040import io.papermc.lib.PaperLib;
041import net.kyori.adventure.audience.Audience;
042import org.bukkit.GameMode;
043import org.bukkit.Sound;
044import org.bukkit.SoundCategory;
045import org.bukkit.WeatherType;
046import org.bukkit.entity.Player;
047import org.bukkit.event.Event;
048import org.bukkit.event.EventException;
049import org.bukkit.event.player.PlayerTeleportEvent;
050import org.bukkit.permissions.PermissionAttachmentInfo;
051import org.bukkit.plugin.RegisteredListener;
052import org.bukkit.potion.PotionEffectType;
053import org.checkerframework.checker.index.qual.NonNegative;
054import org.checkerframework.checker.nullness.qual.NonNull;
055
056import java.util.Set;
057import java.util.UUID;
058
059import static com.sk89q.worldedit.world.gamemode.GameModes.ADVENTURE;
060import static com.sk89q.worldedit.world.gamemode.GameModes.CREATIVE;
061import static com.sk89q.worldedit.world.gamemode.GameModes.SPECTATOR;
062import static com.sk89q.worldedit.world.gamemode.GameModes.SURVIVAL;
063
064public class BukkitPlayer extends PlotPlayer<Player> {
065
066    private static boolean CHECK_EFFECTIVE = true;
067    public final Player player;
068    private String name;
069
070    /**
071     * @param plotAreaManager   PlotAreaManager instance
072     * @param eventDispatcher   EventDispatcher instance
073     * @param player            Bukkit player instance
074     * @param permissionHandler PermissionHandler instance
075     */
076    BukkitPlayer(
077            final @NonNull PlotAreaManager plotAreaManager,
078            final @NonNull EventDispatcher eventDispatcher,
079            final @NonNull Player player,
080            final boolean realPlayer,
081            final @NonNull PermissionHandler permissionHandler
082    ) {
083        super(plotAreaManager, eventDispatcher, permissionHandler);
084        this.player = player;
085        this.setupPermissionProfile();
086        if (realPlayer) {
087            super.populatePersistentMetaMap();
088        }
089    }
090
091    @Override
092    public Actor toActor() {
093        return BukkitAdapter.adapt(player);
094    }
095
096    @Override
097    public Player getPlatformPlayer() {
098        return this.player;
099    }
100
101    @NonNull
102    @Override
103    public UUID getUUID() {
104        if (Settings.UUID.OFFLINE) {
105            if (Settings.UUID.FORCE_LOWERCASE) {
106                return UUID.nameUUIDFromBytes(("OfflinePlayer:" +
107                        getName().toLowerCase()).getBytes(Charsets.UTF_8));
108            } else {
109                return UUID.nameUUIDFromBytes(("OfflinePlayer:" +
110                        getName()).getBytes(Charsets.UTF_8));
111            }
112        }
113        return player.getUniqueId();
114    }
115
116    @Override
117    @NonNegative
118    public long getLastPlayed() {
119        return this.player.getLastSeen();
120    }
121
122    @Override
123    public boolean canTeleport(final @NonNull Location location) {
124        if (!WorldUtil.isValidLocation(location)) {
125            return false;
126        }
127        final org.bukkit.Location to = BukkitUtil.adapt(location);
128        final org.bukkit.Location from = player.getLocation();
129        PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to);
130        callEvent(event);
131        if (event.isCancelled() || !event.getTo().equals(to)) {
132            return false;
133        }
134        event = new PlayerTeleportEvent(player, to, from);
135        callEvent(event);
136        return true;
137    }
138
139    private void callEvent(final @NonNull Event event) {
140        final RegisteredListener[] listeners = event.getHandlers().getRegisteredListeners();
141        for (final RegisteredListener listener : listeners) {
142            if (listener.getPlugin().getName().equals(PlotSquared.platform().pluginName())) {
143                continue;
144            }
145            try {
146                listener.callEvent(event);
147            } catch (final EventException e) {
148                e.printStackTrace();
149            }
150        }
151    }
152
153    @SuppressWarnings("StringSplitter")
154    @Override
155    @NonNegative
156    public int hasPermissionRange(
157            final @NonNull String stub,
158            @NonNegative final int range
159    ) {
160        if (hasPermission(Permission.PERMISSION_ADMIN.toString())) {
161            return Integer.MAX_VALUE;
162        }
163        final String[] nodes = stub.split("\\.");
164        final StringBuilder n = new StringBuilder();
165        // Wildcard check from less specific permission to more specific permission
166        for (int i = 0; i < (nodes.length - 1); i++) {
167            n.append(nodes[i]).append(".");
168            if (!stub.equals(n + Permission.PERMISSION_STAR.toString())) {
169                if (hasPermission(n + Permission.PERMISSION_STAR.toString())) {
170                    return Integer.MAX_VALUE;
171                }
172            }
173        }
174        // Wildcard check for the full permission
175        if (hasPermission(stub + ".*")) {
176            return Integer.MAX_VALUE;
177        }
178        // Permission value cache for iterative check
179        int max = 0;
180        if (CHECK_EFFECTIVE) {
181            boolean hasAny = false;
182            String stubPlus = stub + ".";
183            final Set<PermissionAttachmentInfo> effective = player.getEffectivePermissions();
184            if (!effective.isEmpty()) {
185                for (PermissionAttachmentInfo attach : effective) {
186                    // Ignore all "false" permissions
187                    if (!attach.getValue()) {
188                        continue;
189                    }
190                    String permStr = attach.getPermission();
191                    if (permStr.startsWith(stubPlus)) {
192                        hasAny = true;
193                        String end = permStr.substring(stubPlus.length());
194                        if (MathMan.isInteger(end)) {
195                            int val = Integer.parseInt(end);
196                            if (val > range) {
197                                return val;
198                            }
199                            if (val > max) {
200                                max = val;
201                            }
202                        }
203                    }
204                }
205                if (hasAny) {
206                    return max;
207                }
208                // Workaround
209                for (PermissionAttachmentInfo attach : effective) {
210                    String permStr = attach.getPermission();
211                    if (permStr.startsWith("plots.") && !permStr.equals("plots.use")) {
212                        return max;
213                    }
214                }
215                CHECK_EFFECTIVE = false;
216            }
217        }
218        for (int i = range; i > 0; i--) {
219            if (hasPermission(stub + "." + i)) {
220                return i;
221            }
222        }
223        return max;
224    }
225
226    @Override
227    public void teleport(final @NonNull Location location, final @NonNull TeleportCause cause) {
228        if (!WorldUtil.isValidLocation(location)) {
229            return;
230        }
231        final org.bukkit.Location bukkitLocation =
232                new org.bukkit.Location(BukkitUtil.getWorld(location.getWorldName()), location.getX() + 0.5,
233                        location.getY(), location.getZ() + 0.5, location.getYaw(), location.getPitch()
234                );
235        PaperLib.teleportAsync(player, bukkitLocation, getTeleportCause(cause));
236    }
237
238    @Override
239    public String getName() {
240        if (this.name == null) {
241            this.name = this.player.getName();
242        }
243        return this.name;
244    }
245
246    @Override
247    public void setCompassTarget(Location location) {
248        this.player.setCompassTarget(
249                new org.bukkit.Location(BukkitUtil.getWorld(location.getWorldName()), location.getX(),
250                        location.getY(), location.getZ()
251                ));
252    }
253
254    @Override
255    public Location getLocationFull() {
256        return BukkitUtil.adaptComplete(this.player.getLocation());
257    }
258
259    @Override
260    public void setWeather(final @NonNull PlotWeather weather) {
261        switch (weather) {
262            case CLEAR -> this.player.setPlayerWeather(WeatherType.CLEAR);
263            case RAIN -> this.player.setPlayerWeather(WeatherType.DOWNFALL);
264            case WORLD -> this.player.resetPlayerWeather();
265            default -> {
266                //do nothing as this is PlotWeather.OFF
267            }
268        }
269    }
270
271    @Override
272    public com.sk89q.worldedit.world.gamemode.GameMode getGameMode() {
273        return switch (this.player.getGameMode()) {
274            case ADVENTURE -> ADVENTURE;
275            case CREATIVE -> CREATIVE;
276            case SPECTATOR -> SPECTATOR;
277            default -> SURVIVAL;
278        };
279    }
280
281    @Override
282    public void setGameMode(final com.sk89q.worldedit.world.gamemode.GameMode gameMode) {
283        if (ADVENTURE.equals(gameMode)) {
284            this.player.setGameMode(GameMode.ADVENTURE);
285        } else if (CREATIVE.equals(gameMode)) {
286            this.player.setGameMode(GameMode.CREATIVE);
287        } else if (SPECTATOR.equals(gameMode)) {
288            this.player.setGameMode(GameMode.SPECTATOR);
289        } else {
290            this.player.setGameMode(GameMode.SURVIVAL);
291        }
292    }
293
294    @Override
295    public void setTime(final long time) {
296        if (time != Long.MAX_VALUE) {
297            this.player.setPlayerTime(time, false);
298        } else {
299            this.player.resetPlayerTime();
300        }
301    }
302
303    @Override
304    public boolean getFlight() {
305        return player.getAllowFlight();
306    }
307
308    @Override
309    public void setFlight(boolean fly) {
310        this.player.setAllowFlight(fly);
311    }
312
313    @Override
314    public void playMusic(final @NonNull Location location, final @NonNull ItemType id) {
315        if (id == ItemTypes.AIR) {
316            if (PlotSquared.platform().serverVersion()[1] >= 19) {
317                player.stopSound(SoundCategory.MUSIC);
318                return;
319            }
320            // 1.18 and downwards require a specific Sound to stop (even tho the packet does not??)
321            for (final Sound sound : Sound.values()) {
322                if (sound.name().startsWith("MUSIC_DISC")) {
323                    this.player.stopSound(sound, SoundCategory.MUSIC);
324                }
325            }
326            return;
327        }
328        this.player.playSound(BukkitUtil.adapt(location), Sound.valueOf(BukkitAdapter.adapt(id).name()),
329                SoundCategory.MUSIC, 1f, 1f
330        );
331    }
332
333    @SuppressWarnings("deprecation") // Needed for Spigot compatibility
334    @Override
335    public void kick(final String message) {
336        this.player.kickPlayer(message);
337    }
338
339    @Override
340    public void stopSpectating() {
341        if (getGameMode() == SPECTATOR) {
342            this.player.setSpectatorTarget(null);
343        }
344    }
345
346    @Override
347    public boolean isBanned() {
348        return this.player.isBanned();
349    }
350
351    @Override
352    public @NonNull Audience getAudience() {
353        return BukkitUtil.BUKKIT_AUDIENCES.player(this.player);
354    }
355
356    @Override
357    public void removeEffect(@NonNull String name) {
358        PotionEffectType type = PotionEffectType.getByName(name);
359        if (type != null) {
360            player.removePotionEffect(type);
361        }
362    }
363
364    @Override
365    public boolean canSee(final PlotPlayer<?> other) {
366        if (other instanceof ConsolePlayer) {
367            return true;
368        } else {
369            return this.player.canSee(((BukkitPlayer) other).getPlatformPlayer());
370        }
371    }
372
373    /**
374     * Convert from PlotSquared's {@link TeleportCause} to Bukkit's {@link PlayerTeleportEvent.TeleportCause}
375     *
376     * @param cause PlotSquared teleport cause to convert
377     * @return Bukkit's equivalent teleport cause
378     */
379    public PlayerTeleportEvent.TeleportCause getTeleportCause(final @NonNull TeleportCause cause) {
380        if (TeleportCause.CauseSets.COMMAND.contains(cause)) {
381            return PlayerTeleportEvent.TeleportCause.COMMAND;
382        } else if (cause == TeleportCause.UNKNOWN) {
383            return PlayerTeleportEvent.TeleportCause.UNKNOWN;
384        }
385        return PlayerTeleportEvent.TeleportCause.PLUGIN;
386    }
387
388}