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