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