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        for (int i = 0; i < (nodes.length - 1); i++) {
162            n.append(nodes[i]).append(".");
163            if (!stub.equals(n + Permission.PERMISSION_STAR.toString())) {
164                if (hasPermission(n + Permission.PERMISSION_STAR.toString())) {
165                    return Integer.MAX_VALUE;
166                }
167            }
168        }
169        if (hasPermission(stub + ".*")) {
170            return Integer.MAX_VALUE;
171        }
172        int max = 0;
173        if (CHECK_EFFECTIVE) {
174            boolean hasAny = false;
175            String stubPlus = stub + ".";
176            final Set<PermissionAttachmentInfo> effective = player.getEffectivePermissions();
177            if (!effective.isEmpty()) {
178                for (PermissionAttachmentInfo attach : effective) {
179                    // Ignore all "false" permissions
180                    if (!attach.getValue()) {
181                        continue;
182                    }
183                    String permStr = attach.getPermission();
184                    if (permStr.startsWith(stubPlus)) {
185                        hasAny = true;
186                        String end = permStr.substring(stubPlus.length());
187                        if (MathMan.isInteger(end)) {
188                            int val = Integer.parseInt(end);
189                            if (val > range) {
190                                return val;
191                            }
192                            if (val > max) {
193                                max = val;
194                            }
195                        }
196                    }
197                }
198                if (hasAny) {
199                    return max;
200                }
201                // Workaround
202                for (PermissionAttachmentInfo attach : effective) {
203                    String permStr = attach.getPermission();
204                    if (permStr.startsWith("plots.") && !permStr.equals("plots.use")) {
205                        return max;
206                    }
207                }
208                CHECK_EFFECTIVE = false;
209            }
210        }
211        for (int i = range; i > 0; i--) {
212            if (hasPermission(stub + "." + i)) {
213                return i;
214            }
215        }
216        return max;
217    }
218
219    @Override
220    public void teleport(final @NonNull Location location, final @NonNull TeleportCause cause) {
221        if (Math.abs(location.getX()) >= 30000000 || Math.abs(location.getZ()) >= 30000000) {
222            return;
223        }
224        final org.bukkit.Location bukkitLocation =
225                new org.bukkit.Location(BukkitUtil.getWorld(location.getWorldName()), location.getX() + 0.5,
226                        location.getY(), location.getZ() + 0.5, location.getYaw(), location.getPitch()
227                );
228        PaperLib.teleportAsync(player, bukkitLocation, getTeleportCause(cause));
229    }
230
231    @Override
232    public String getName() {
233        if (this.name == null) {
234            this.name = this.player.getName();
235        }
236        return this.name;
237    }
238
239    @Override
240    public void setCompassTarget(Location location) {
241        this.player.setCompassTarget(
242                new org.bukkit.Location(BukkitUtil.getWorld(location.getWorldName()), location.getX(),
243                        location.getY(), location.getZ()
244                ));
245    }
246
247    @Override
248    public Location getLocationFull() {
249        return BukkitUtil.adaptComplete(this.player.getLocation());
250    }
251
252    @Override
253    public void setWeather(final @NonNull PlotWeather weather) {
254        switch (weather) {
255            case CLEAR -> this.player.setPlayerWeather(WeatherType.CLEAR);
256            case RAIN -> this.player.setPlayerWeather(WeatherType.DOWNFALL);
257            case WORLD -> this.player.resetPlayerWeather();
258            default -> {
259                //do nothing as this is PlotWeather.OFF
260            }
261        }
262    }
263
264    @Override
265    public com.sk89q.worldedit.world.gamemode.GameMode getGameMode() {
266        return switch (this.player.getGameMode()) {
267            case ADVENTURE -> ADVENTURE;
268            case CREATIVE -> CREATIVE;
269            case SPECTATOR -> SPECTATOR;
270            default -> SURVIVAL;
271        };
272    }
273
274    @Override
275    public void setGameMode(final com.sk89q.worldedit.world.gamemode.GameMode gameMode) {
276        if (ADVENTURE.equals(gameMode)) {
277            this.player.setGameMode(GameMode.ADVENTURE);
278        } else if (CREATIVE.equals(gameMode)) {
279            this.player.setGameMode(GameMode.CREATIVE);
280        } else if (SPECTATOR.equals(gameMode)) {
281            this.player.setGameMode(GameMode.SPECTATOR);
282        } else {
283            this.player.setGameMode(GameMode.SURVIVAL);
284        }
285    }
286
287    @Override
288    public void setTime(final long time) {
289        if (time != Long.MAX_VALUE) {
290            this.player.setPlayerTime(time, false);
291        } else {
292            this.player.resetPlayerTime();
293        }
294    }
295
296    @Override
297    public boolean getFlight() {
298        return player.getAllowFlight();
299    }
300
301    @Override
302    public void setFlight(boolean fly) {
303        this.player.setAllowFlight(fly);
304    }
305
306    @Override
307    public void playMusic(final @NonNull Location location, final @NonNull ItemType id) {
308        if (id == ItemTypes.AIR) {
309            // Let's just stop all the discs because why not?
310            for (final Sound sound : Arrays.stream(Sound.values())
311                    .filter(sound -> sound.name().contains("DISC")).toList()) {
312                player.stopSound(sound);
313            }
314            // this.player.playEffect(BukkitUtil.getLocation(location), Effect.RECORD_PLAY, Material.AIR);
315        } else {
316            // this.player.playEffect(BukkitUtil.getLocation(location), Effect.RECORD_PLAY, id.to(Material.class));
317            this.player.playSound(BukkitUtil.adapt(location),
318                    Sound.valueOf(BukkitAdapter.adapt(id).name()), Float.MAX_VALUE, 1f
319            );
320        }
321    }
322
323    @SuppressWarnings("deprecation") // Needed for Spigot compatibility
324    @Override
325    public void kick(final String message) {
326        this.player.kickPlayer(message);
327    }
328
329    @Override
330    public void stopSpectating() {
331        if (getGameMode() == SPECTATOR) {
332            this.player.setSpectatorTarget(null);
333        }
334    }
335
336    @Override
337    public boolean isBanned() {
338        return this.player.isBanned();
339    }
340
341    @Override
342    public @NonNull Audience getAudience() {
343        return BukkitUtil.BUKKIT_AUDIENCES.player(this.player);
344    }
345
346    @Override
347    public void removeEffect(@NonNull String name) {
348        PotionEffectType type = PotionEffectType.getByName(name);
349        if (type != null) {
350            player.removePotionEffect(type);
351        }
352    }
353
354    @Override
355    public boolean canSee(final PlotPlayer<?> other) {
356        if (other instanceof ConsolePlayer) {
357            return true;
358        } else {
359            return this.player.canSee(((BukkitPlayer) other).getPlatformPlayer());
360        }
361    }
362
363    /**
364     * Convert from PlotSquared's {@link TeleportCause} to Bukkit's {@link PlayerTeleportEvent.TeleportCause}
365     *
366     * @param cause PlotSquared teleport cause to convert
367     * @return Bukkit's equivalent teleport cause
368     */
369    public PlayerTeleportEvent.TeleportCause getTeleportCause(final @NonNull TeleportCause cause) {
370        if (TeleportCause.CauseSets.COMMAND.contains(cause)) {
371            return PlayerTeleportEvent.TeleportCause.COMMAND;
372        } else if (cause == TeleportCause.UNKNOWN) {
373            return PlayerTeleportEvent.TeleportCause.UNKNOWN;
374        }
375        return PlayerTeleportEvent.TeleportCause.PLUGIN;
376    }
377
378}