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.core.util.placeholders;
020
021import com.google.common.base.Function;
022import com.google.common.base.Preconditions;
023import com.google.common.collect.Maps;
024import com.google.inject.Inject;
025import com.google.inject.Singleton;
026import com.plotsquared.core.PlotSquared;
027import com.plotsquared.core.configuration.Settings;
028import com.plotsquared.core.configuration.caption.LocaleHolder;
029import com.plotsquared.core.configuration.caption.TranslatableCaption;
030import com.plotsquared.core.player.MetaDataAccess;
031import com.plotsquared.core.player.PlayerMetaDataKeys;
032import com.plotsquared.core.player.PlotPlayer;
033import com.plotsquared.core.plot.Plot;
034import com.plotsquared.core.plot.flag.GlobalFlagContainer;
035import com.plotsquared.core.plot.flag.PlotFlag;
036import com.plotsquared.core.plot.flag.implementations.DoneFlag;
037import com.plotsquared.core.plot.flag.implementations.ServerPlotFlag;
038import com.plotsquared.core.util.EventDispatcher;
039import com.plotsquared.core.util.PlayerManager;
040import com.plotsquared.core.util.query.PlotQuery;
041import net.kyori.adventure.text.Component;
042import org.checkerframework.checker.nullness.qual.NonNull;
043import org.checkerframework.checker.nullness.qual.Nullable;
044
045import java.math.BigDecimal;
046import java.math.RoundingMode;
047import java.text.SimpleDateFormat;
048import java.util.Collection;
049import java.util.Collections;
050import java.util.Locale;
051import java.util.Map;
052import java.util.Set;
053import java.util.TimeZone;
054import java.util.UUID;
055import java.util.concurrent.TimeUnit;
056import java.util.function.BiFunction;
057import java.util.stream.Collectors;
058
059/**
060 * Registry that contains {@link Placeholder placeholders}
061 */
062@Singleton
063public final class PlaceholderRegistry {
064
065    private final Map<String, Placeholder> placeholders;
066    private final EventDispatcher eventDispatcher;
067
068    @Inject
069    public PlaceholderRegistry(final @NonNull EventDispatcher eventDispatcher) {
070        this.placeholders = Maps.newHashMap();
071        this.eventDispatcher = eventDispatcher;
072        this.registerDefault();
073    }
074
075    /**
076     * Converts a {@link Component} into a legacy-formatted string.
077     *
078     * @param caption      the caption key.
079     * @param localeHolder the locale holder to get the component for
080     * @return a legacy-formatted string.
081     */
082    private static String legacyComponent(TranslatableCaption caption, LocaleHolder localeHolder) {
083        return PlotSquared.platform().toLegacyPlatformString(caption.toComponent(localeHolder).asComponent());
084    }
085
086    private void registerDefault() {
087        final GlobalFlagContainer globalFlagContainer = GlobalFlagContainer.getInstance();
088        for (final PlotFlag<?, ?> flag : globalFlagContainer.getRecognizedPlotFlags()) {
089            this.registerPlaceholder(new PlotFlagPlaceholder(flag, true));
090            this.registerPlaceholder(new PlotFlagPlaceholder(flag, false));
091        }
092        GlobalFlagContainer.getInstance().subscribe((flag, type) -> {
093            this.registerPlaceholder(new PlotFlagPlaceholder(flag, true));
094            this.registerPlaceholder(new PlotFlagPlaceholder(flag, false));
095        });
096        this.createPlaceholder("world_name", player -> player.getLocation().getWorldName());
097        this.createPlaceholder("has_plot", player -> player.getPlotCount() > 0 ? "true" : "false");
098        this.createPlaceholder("allowed_plot_count", (player) -> {
099            if (player.getAllowedPlots() >= Integer.MAX_VALUE) { // Beautifies cases with '*' permission
100                return legacyComponent(TranslatableCaption.of("info.infinite"), player);
101            }
102            return Integer.toString(player.getAllowedPlots());
103        });
104        this.createPlaceholder("base_plot_count", player -> Integer.toString(PlotQuery.newQuery()
105                .ownedBy(player)
106                .whereBasePlot()
107                .thatPasses(plot -> !DoneFlag.isDone(plot))
108                .count())
109        );
110        this.createPlaceholder("plot_count", player -> Integer.toString(player.getPlotCount()));
111        this.createPlaceholder("currentplot_alias", (player, plot) -> {
112            if (plot.getAlias().isEmpty()) {
113                return legacyComponent(TranslatableCaption.of("info.none"), player);
114            }
115            return plot.getAlias();
116        });
117        this.createPlaceholder("currentplot_owner", (player, plot) -> {
118            if (plot.getFlag(ServerPlotFlag.class)) {
119                return legacyComponent(TranslatableCaption.of("info.server"), player);
120            }
121            final UUID plotOwner = plot.getOwnerAbs();
122            if (plotOwner == null) {
123                return legacyComponent(TranslatableCaption.of("generic.generic_unowned"), player);
124            }
125            try {
126                return PlotSquared.platform().playerManager().getUsernameCaption(plotOwner)
127                        .get(Settings.UUID.BLOCKING_TIMEOUT, TimeUnit.MILLISECONDS).getComponent(player);
128            } catch (final Exception ignored) {
129            }
130            return legacyComponent(TranslatableCaption.of("info.unknown"), player);
131        });
132        this.createPlaceholder("currentplot_owners", (player, plot) -> {
133            if (plot.getFlag(ServerPlotFlag.class)) {
134                return legacyComponent(TranslatableCaption.of("info.server"), player);
135            }
136            final Set<UUID> plotOwners = plot.getOwners();
137            if (plotOwners.isEmpty()) {
138                return legacyComponent(TranslatableCaption.of("generic.generic_unowned"), player);
139            }
140            return plotOwners.stream().map(PlotSquared.platform().playerManager()::getUsernameCaption).map(f -> {
141                try {
142                    return f.get(Settings.UUID.BLOCKING_TIMEOUT, TimeUnit.MILLISECONDS).getComponent(player);
143                } catch (final Exception ignored) {
144                    return legacyComponent(TranslatableCaption.of("info.unknown"), player);
145                }
146            }).collect(Collectors.joining(", "));
147        });
148        this.createPlaceholder("currentplot_members", (player, plot) -> {
149            if (plot.getMembers().isEmpty() && plot.getTrusted().isEmpty()) {
150                return legacyComponent(TranslatableCaption.of("info.none"), player);
151            }
152            return String.valueOf(plot.getMembers().size() + plot.getTrusted().size());
153        });
154        this.createPlaceholder("currentplot_members_added", (player, plot) -> {
155            if (plot.getMembers().isEmpty()) {
156                return legacyComponent(TranslatableCaption.of("info.none"), player);
157            }
158            return String.valueOf(plot.getMembers().size());
159        });
160        this.createPlaceholder("currentplot_members_trusted", (player, plot) -> {
161            if (plot.getTrusted().isEmpty()) {
162                return legacyComponent(TranslatableCaption.of("info.none"), player);
163            }
164            return String.valueOf(plot.getTrusted().size());
165        });
166        this.createPlaceholder("currentplot_members_denied", (player, plot) -> {
167            if (plot.getDenied().isEmpty()) {
168                return legacyComponent(TranslatableCaption.of("info.none"), player);
169            }
170            return String.valueOf(plot.getDenied().size());
171        });
172        this.createPlaceholder("currentplot_members_trusted_list", (player, plot) -> {
173            if (plot.getTrusted().isEmpty()) {
174                return legacyComponent(TranslatableCaption.of("info.none"), player);
175            }
176            return PlotSquared.platform().toLegacyPlatformString(
177                    PlayerManager.getPlayerList(plot.getTrusted(), player));
178        });
179        this.createPlaceholder("currentplot_members_added_list", (player, plot) -> {
180            if (plot.getMembers().isEmpty()) {
181                return legacyComponent(TranslatableCaption.of("info.none"), player);
182            }
183            return PlotSquared.platform().toLegacyPlatformString(
184                    PlayerManager.getPlayerList(plot.getMembers(), player));
185        });
186        this.createPlaceholder("currentplot_members_denied_list", (player, plot) -> {
187            if (plot.getDenied().isEmpty()) {
188                return legacyComponent(TranslatableCaption.of("info.none"), player);
189            }
190            return PlotSquared.platform().toLegacyPlatformString(
191                    PlayerManager.getPlayerList(plot.getDenied(), player));
192        });
193        this.createPlaceholder("currentplot_creationdate", (player, plot) -> {
194            if (plot.getTimestamp() == 0 || !plot.hasOwner()) {
195                return legacyComponent(TranslatableCaption.of("info.unknown"), player);
196            }
197            long creationDate = plot.getTimestamp();
198            SimpleDateFormat sdf = new SimpleDateFormat(Settings.Timeformat.DATE_FORMAT);
199            sdf.setTimeZone(TimeZone.getTimeZone(Settings.Timeformat.TIME_ZONE));
200            return sdf.format(creationDate);
201        });
202        this.createPlaceholder("currentplot_can_build", (player, plot) ->
203                plot.isAdded(player.getUUID()) ? "true" : "false");
204        this.createPlaceholder("currentplot_x", (player, plot) -> Integer.toString(plot.getId().getX()));
205        this.createPlaceholder("currentplot_y", (player, plot) -> Integer.toString(plot.getId().getY()));
206        this.createPlaceholder("currentplot_xy", (player, plot) -> plot.getId().toString());
207        this.createPlaceholder("currentplot_abs_x", (player, plot) -> Integer.toString(plot.getId().getX()), true);
208        this.createPlaceholder("currentplot_abs_y", (player, plot) -> Integer.toString(plot.getId().getY()), true);
209        this.createPlaceholder("currentplot_abs_xy", (player, plot) -> plot.getId().toString(), true);
210        this.createPlaceholder("currentplot_rating", (player, plot) -> {
211            if (Double.isNaN(plot.getAverageRating())) {
212                return legacyComponent(TranslatableCaption.of("placeholder.nan"), player);
213            }
214            BigDecimal roundRating = BigDecimal.valueOf(plot.getAverageRating()).setScale(2, RoundingMode.HALF_UP);
215            if (!Settings.General.SCIENTIFIC) {
216                return String.valueOf(roundRating);
217            } else {
218                return Double.toString(plot.getAverageRating());
219            }
220        });
221        this.createPlaceholder("currentplot_biome", (player, plot) -> plot.getBiomeSynchronous().toString());
222        this.createPlaceholder("currentplot_size", (player, plot) -> String.valueOf(plot.getConnectedPlots().size()));
223        this.createPlaceholder("total_grants", player -> {
224            try (final MetaDataAccess<Integer> metaDataAccess = player.accessPersistentMetaData(PlayerMetaDataKeys.PERSISTENT_GRANTED_PLOTS)) {
225                return Integer.toString(metaDataAccess.get().orElse(0));
226            }
227        });
228        this.createPlaceholder("server_plot_count", player -> Integer.toString(PlotQuery.newQuery()
229                .allPlots()
230                .count())
231        );
232        this.createPlaceholder("server_base_plot_count", player -> Integer.toString(PlotQuery.newQuery()
233                .allPlots()
234                .whereBasePlot()
235                .count())
236        );
237    }
238
239    /**
240     * Create a functional placeholder
241     *
242     * @param key                 Placeholder key
243     * @param placeholderFunction Placeholder generator. Cannot return null
244     */
245    @SuppressWarnings("ALL")
246    public void createPlaceholder(
247            final @NonNull String key,
248            final @NonNull Function<PlotPlayer<?>, String> placeholderFunction
249    ) {
250        this.registerPlaceholder(new Placeholder(key) {
251            @Override
252            public @NonNull String getValue(final @NonNull PlotPlayer<?> player) {
253                return placeholderFunction.apply(player);
254            }
255        });
256    }
257
258    /**
259     * Create a functional placeholder
260     *
261     * @param key                 Placeholder key
262     * @param placeholderFunction Placeholder generator. Cannot return null
263     */
264    public void createPlaceholder(
265            final @NonNull String key,
266            final @NonNull BiFunction<PlotPlayer<?>, Plot, String> placeholderFunction
267    ) {
268        this.createPlaceholder(key, placeholderFunction, false);
269    }
270
271    /**
272     * Create a functional placeholder
273     *
274     * @param key                 Placeholder key
275     * @param placeholderFunction Placeholder generator. Cannot return null
276     * @param requireAbsolute     If the plot given to the placeholder should be the absolute (not base) plot
277     * @since 7.5.9
278     */
279    public void createPlaceholder(
280            final @NonNull String key,
281            final @NonNull BiFunction<PlotPlayer<?>, Plot, String> placeholderFunction,
282            final boolean requireAbsolute
283    ) {
284        this.registerPlaceholder(new PlotSpecificPlaceholder(key, requireAbsolute) {
285            @Override
286            public @NonNull String getValue(final @NonNull PlotPlayer<?> player, final @NonNull Plot plot) {
287                return placeholderFunction.apply(player, plot);
288            }
289        });
290    }
291
292    /**
293     * Register a placeholder
294     *
295     * @param placeholder Placeholder instance
296     */
297    public void registerPlaceholder(final @NonNull Placeholder placeholder) {
298        final Placeholder previous = this.placeholders
299                .put(
300                        placeholder.getKey().toLowerCase(Locale.ENGLISH),
301                        Preconditions.checkNotNull(placeholder, "Placeholder may not be null")
302                );
303        if (previous == null) {
304            this.eventDispatcher.callGenericEvent(new PlaceholderAddedEvent(placeholder));
305        }
306    }
307
308    /**
309     * Get a placeholder instance from its key
310     *
311     * @param key Placeholder key
312     * @return Placeholder value
313     */
314    public @Nullable Placeholder getPlaceholder(final @NonNull String key) {
315        return this.placeholders.get(
316                Preconditions.checkNotNull(key, "Key may not be null").toLowerCase(Locale.ENGLISH));
317    }
318
319    /**
320     * Get the placeholder value evaluated for a player, and catch and deal with any problems
321     * occurring while doing so
322     *
323     * @param key    Placeholder key
324     * @param player Player to evaluate for
325     * @return Replacement value
326     */
327    public @NonNull String getPlaceholderValue(
328            final @NonNull String key,
329            final @NonNull PlotPlayer<?> player
330    ) {
331        final Placeholder placeholder = getPlaceholder(key);
332        if (placeholder == null) {
333            return "";
334        }
335        String placeholderValue = "";
336        try {
337            placeholderValue = placeholder.getValue(player);
338            // If a placeholder for some reason decides to be disobedient, we catch it here
339            if (placeholderValue == null) {
340                new RuntimeException(String
341                        .format("Placeholder '%s' returned null for player '%s'", placeholder.getKey(),
342                                player.getName()
343                        )).printStackTrace();
344            }
345        } catch (final Exception exception) {
346            new RuntimeException(String
347                    .format("Placeholder '%s' failed to evalulate for player '%s'",
348                            placeholder.getKey(), player.getName()
349                    ), exception).printStackTrace();
350        }
351        return placeholderValue;
352    }
353
354    /**
355     * Get all placeholders
356     *
357     * @return Unmodifiable collection of placeholders
358     */
359    public @NonNull Collection<Placeholder> getPlaceholders() {
360        return Collections.unmodifiableCollection(this.placeholders.values());
361    }
362
363    /**
364     * Event called when a new {@link Placeholder} has been added
365     */
366    public record PlaceholderAddedEvent(
367            Placeholder placeholder
368    ) {
369
370    }
371
372}