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}