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