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