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