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