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