Compare commits

...
63 Commits
Author SHA1 Message Date
PiggyPiglet 96701da560 move folder debug msg above listfiles 2026-07-06 06:59:17 +08:00
PiggyPiglet ccf520a1b0 debug msgs 2026-07-06 06:54:38 +08:00
Funnycube 7d21d2f1d7 2.12.4 dev 2026-07-03 21:49:43 +10:00
Funnycube 31a050af54 2.12.3 release 2026-07-03 21:21:05 +10:00
Andre_601 6dc238c45d Merge pull request #1221 from Andre601/master
Add dedicated comment action for failed validation
2026-07-01 14:12:32 +02:00
PiggyPiglet 23a00370ac Merge pull request #1235 from PlaceholderAPI/feat/expansion-logging-improvements
fix(logging): improve local expansion failure diagnostics
2026-07-01 19:53:21 +08:00
darbyjack c5c75a5e8c fix(logging): avoid msg during local expansion manager initialization 2026-06-30 20:13:43 -05:00
darbyjack ecc4ca002c improve(logging): log expansion instantiation failures with stack trace 2026-06-30 20:04:08 -05:00
PiggyPiglet 4845e35067 Merge pull request #1227 from PlaceholderAPI/fix/version-parsing
fix(parsing): adjust parsing to support paper dev builds better
2026-06-20 19:13:20 +08:00
darbyjack a41bf76656 feat(version): implement version resolver that utilizes server build info from paper 2026-06-16 17:31:03 -05:00
darbyjack 8fcabecf63 fix(parsing): adjust parsing to support paper dev builds better 2026-06-16 16:15:04 -05:00
Andre_601 e8306ed434 Delete .github/workflows/pr_wiki_validation.yml 2026-05-28 00:19:18 +02:00
Andre_601 7da63d2d58 Merge pull request #2 from Andre601/feature/wiki-improve-validation
Feature/wiki improve validation
2026-05-28 00:05:41 +02:00
Andre_601 ff1d8bb74c Separate comment into its own workflow. 2026-05-27 23:52:08 +02:00
Andre_601 532c98a41d Improve Wiki validation run 2026-05-27 23:35:42 +02:00
Gabriel Dumitru 79fabecbf3 ci: change trigger for pr_wiki_validation workflow 2026-05-25 11:34:24 +03:00
Funnycube eacac0ca98 Disable PR wiki validation workflow 2026-05-24 14:50:43 +10:00
PiggyPiglet feb4eff237 Merge pull request #1205 from PlaceholderAPI/feat/new-chars-replacer
Feat/new chars replacer
2026-02-25 21:13:49 +08:00
PiggyPiglet 7286d8849b only flag spaces if we haven't met an underscore 2026-02-25 21:13:05 +08:00
PiggyPiglet 1807b92505 choco chars replacer 2026-02-20 21:19:25 +08:00
PiggyPiglet 3c82c6cdd7 revert #1163 2026-02-20 21:18:22 +08:00
PiggyPiglet 17ad663257 fix papi version header 2026-02-14 18:07:15 +08:00
PiggyPiglet 984ec7f7b9 add papi version to user agent 2026-02-14 17:24:31 +08:00
PiggyPiglet c8f73a5940 add custom user agent for http requests 2026-02-14 17:19:45 +08:00
PiggyPiglet 81675c1fd6 Merge pull request #1188 from VaultedMC/master
PAPIComponents Deserializer Support
2026-02-13 09:56:30 +08:00
Justin 69cef6f72b Renamed serializer to deserializer 2026-02-12 20:38:31 -05:00
Justin ca2bd9f19a Merge remote-tracking branch 'upstream/master' 2026-02-12 20:32:37 -05:00
Justin b331145c15 Added serializer support to placeholder replacement methods. 2026-02-12 20:30:30 -05:00
PiggyPiglet b085e49062 Reimplement Blitz' replacer change for new replacer 2026-02-12 21:00:57 +08:00
PiggyPiglet 43f6a517af Merge pull request #1163 from ichoco-milk/master
Refactor CharsReplacer & PlaceholderAPI for better performance and reduced memory allocation
2026-02-12 20:35:41 +08:00
PiggyPiglet 59769c2d93 Merge branch 'master' into master 2026-02-12 20:33:19 +08:00
PiggyPiglet 354aebe8df Merge pull request #1178 from PlaceholderAPI/fix/argumentless-expansions
Make setPlaceholders behavior consistent with setRelationalPlaceholders
2026-02-10 11:21:25 +08:00
BlitzOffline 566beb48c5 Fix copyright 2026-02-09 17:06:35 +02:00
BlitzOffline 64b3d6fa68 Make setPlaceholders behavior consistent with setRelationalPlaceholders 2026-02-09 17:05:20 +02:00
PiggyPiglet 975b1ac3ca Merge pull request #1089 from PlaceholderAPI/feature/update-issue-template
Update Common Issues page link in issue-template
2026-02-09 22:42:17 +08:00
Andre_601 7b8550c3f1 Fix wrong install command and move comment step 2026-02-09 15:28:11 +01:00
Andre_601 f267887b5e Use wiki PR validation on main branch. 2026-02-09 15:20:59 +01:00
PiggyPiglet e187eb6402 2.12.3-dev 2026-02-09 01:05:57 +08:00
PiggyPiglet 0d1a356e0f ExpansionSafety: Fix NPE & Only check files not directories 2026-02-09 01:02:32 +08:00
PiggyPiglet 0a53559ae1 2.12.2 release 2026-02-08 20:09:01 +08:00
PiggyPiglet 6c5f19b61d Fix #1167 2026-02-08 20:02:02 +08:00
ichocomilk 407b9f5e54 Simplify getExpansion method by removing unnecessary locking 2026-02-04 20:22:01 -03:00
ichocomilk 469997e114 Refactor placeholder methods to use ArrayList for compatibility | I used immutable list by error :( 2026-02-04 20:17:44 -03:00
ichocomilk 8185b7bfe9 Merge remote-tracking branch 'origin/master' 2026-02-04 19:50:07 -03:00
ichocomilk fe15e4ed7a Update Gradle dependencies and add benchmarks 2026-02-04 19:49:54 -03:00
iChocoMilk 0f35362a0c Fix PlaceholderAPI#containsBracketPlaceholders variable name 2026-02-04 01:50:19 -03:00
ichocomilk 9a4fa18304 Enhance placeholder translation performance and improve code clarity 2026-02-04 01:26:39 -03:00
ichocomilk cb5d6c0895 Optimize containsPlaceholders 2026-02-04 00:47:58 -03:00
PiggyPiglet 9022daf07f 2.12.2-dev 2026-02-03 17:17:36 +08:00
PiggyPiglet 675b305cac 2.12.1 release 2026-02-03 17:16:29 +08:00
PiggyPiglet d49c76c560 exclamation marks are important 2026-02-03 17:15:46 +08:00
PiggyPiglet 13e492cf44 Show version in papi ecloud list hover 2026-02-03 16:55:12 +08:00
PiggyPiglet d561afbb63 Use modrinth for update checker 2026-02-03 16:52:25 +08:00
PiggyPiglet 2a3f4482a0 check for perms on all tab complete 2026-02-03 16:28:42 +08:00
Funnycube 4ee2840a0a Update download and server usage statistics in README 2026-02-02 22:24:12 +11:00
PiggyPiglet c52d117f12 2.12.1 dev 2026-02-02 18:28:07 +08:00
PiggyPiglet e307aba414 2.12.0 release 2026-02-02 17:58:26 +08:00
PiggyPiglet 5ea5a18fe8 fix config option path 2026-02-02 17:43:41 +08:00
PiggyPiglet 9c1db4b48a fix newlines in ecloud command error & restrict api to bukkit platform 2026-02-02 17:29:15 +08:00
PiggyPiglet b233c92ca1 Add warning message to failed ecloud download 2026-02-02 17:10:50 +08:00
PiggyPiglet 1b1d2e61b9 Add config option to use adventure replacer, add bracket {} support 2026-02-02 16:35:13 +08:00
PiggyPiglet 2dc5b93133 WIP custom component replacer no regex!! 2026-01-21 22:14:46 +08:00
Andre_601 3684e32ac0 Update Common Issues page link in issue-template 2024-12-04 15:00:51 +01:00
29 changed files with 1179 additions and 204 deletions

No files matched your search

+7 -5
View File
@@ -18,13 +18,13 @@ body:
label: Confirmation
description: Please make sure to have followed the following checks.
options:
- label: My issue isn't already found on the Issue tracker.
- label: "My issue isn't already found on the Issue tracker."
required: true
- label: My issue is about **PlaceholderAPI** and not any expansion or external plugin
- label: "My issue is about **PlaceholderAPI** and not any expansion or external plugin."
required: true
- label: The issue isn't already fixed in a Spigot Release or Development Build.
- label: "The issue isn't already fixed in a Spigot Release or Development Build."
required: true
- label: The [Common Issues](https://github.com/PlaceholderAPI/PlaceholderAPI/wiki/Common-Issues) page doesn't mention this issue.
- label: "The [Common Issues](https://wiki.placeholderapi.com/common-issues/) page doesn't mention this issue."
required: true
- type: dropdown
attributes:
@@ -85,6 +85,8 @@ body:
description: |-
Get the latest content of your `latest.log` file an upload it to https://paste.helpch.at
Take the generated URL and paste it into this field.
**Always provide the full `latest.log` and not just parts of it or just the error (if any)!**
placeholder: "https://paste.helpch.at/latest.log"
- type: input
id: "error"
@@ -99,5 +101,5 @@ body:
description: |-
Add any extra info you think is nessesary for this Bug report.
- If you selected `API Bug` will you need to include code-examples here to reproduce the issue.
- If you selected `Plugin/Server Incompatability` should you include extra Server info such as a Timings or Spark-Report or info about the plugin in question.
- If you selected `Plugin/Server Incompatability` should you include extra Server info such as Spark-Report or info about the plugin in question.
placeholder: "Put any extra info you like into this field..."
@@ -0,0 +1,34 @@
#
# A separate Workflow to comment on Pull requests
# for failed Wiki validation runs.
#
# This is to avoid code execution injected by
# extensions, plugins or hooks in MkDocs, that could
# abuse write permissions.
#
name: "Comment on failed Validation"
on:
workflow_run:
workflows:
- "Validate Wiki Build"
types:
- completed
permissions:
pull-requests: write
jobs:
comment:
if: github.event.workflow_run.conclusion == 'failure'
runs-on: ubuntu-latest
steps:
- name: Comment on Pull request
uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
body: |
## Build validation failed
Something went wrong while running a test-build with this Pull request's changes.
Please check the [Workflow Run Logs](${{ github.event.workflow_run.html_url }}) for any details.
+1 -1
View File
@@ -32,7 +32,7 @@
Support for specific plugins are provided either by the plugin itself or through expansions. The expansions may be downloaded in-game through the PAPI Expansion Cloud. There are currently over 240+ expansions that support a wide variety of plugins, such as Essentials, Factions, LuckPerms, and Vault.
PlaceholderAPI has been downloaded over 1,700,000 times on Spigot and has been used concurrently on over 45,000 servers, which makes it a must-have for a server of any type or scale.
PlaceholderAPI has been downloaded over 2,000,000 times on Spigot and has been used concurrently on over 50,000 servers, which makes it a must-have for a server of any type or scale.
## Contribute
If you would like to contribute towards PlaceholderAPI should you take a look at our [Contributing file][contributing] for the ins and outs on how you can do that and what you need to keep in mind.
+9 -7
View File
@@ -4,11 +4,12 @@ plugins {
`java-library`
`maven-publish`
// id("com.github.hierynomus.license") version "0.16.1"
id("io.github.goooler.shadow") version "8.1.7"
id("com.gradleup.shadow") version "9.3.1"
id("me.champeau.jmh") version "0.7.2"
}
group = "me.clip"
version = "2.12.0-DEV-${System.getProperty("BUILD_NUMBER")}"
version = "2.12.4-DEV-${System.getProperty("BUILD_NUMBER")}"
description = "An awesome placeholder provider!"
@@ -41,12 +42,13 @@ dependencies {
compileOnly("dev.folia:folia-api:1.21.11-R0.1-SNAPSHOT")
compileOnlyApi("org.jetbrains:annotations:23.0.0")
testImplementation("org.openjdk.jmh:jmh-core:1.32")
testImplementation("org.openjdk.jmh:jmh-generator-annprocess:1.32")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.8.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
}
jmh("org.openjdk.jmh:jmh-core:1.37")
jmh("org.openjdk.jmh:jmh-generator-annprocess:1.37")
jmhAnnotationProcessor("org.openjdk.jmh:jmh-generator-annprocess:1.37")
testImplementation("org.junit.jupiter:junit-jupiter:6.0.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -0,0 +1,94 @@
/*
* This file is part of PlaceholderAPI
*
* PlaceholderAPI
* Copyright (c) 2015 - 2026 PlaceholderAPI Team
*
* PlaceholderAPI free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlaceholderAPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.clip.placeholderapi;
import com.google.common.collect.ImmutableMap;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import me.clip.placeholderapi.replacer.CharsReplacer;
import me.clip.placeholderapi.replacer.OldCharsReplacer;
import me.clip.placeholderapi.replacer.Replacer;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface Values {
String SMALL_TEXT = "My name is %player_name%";
String LARGE_TEXT = "My name is %player_name% and my location is (%player_x%, %player_y%, %player_z%), this placeholder is invalid %server_name%";
ImmutableMap<String, PlaceholderExpansion> PLACEHOLDERS = ImmutableMap.<String, PlaceholderExpansion>builder()
.put("player", new MockPlayerPlaceholderExpansion())
.build();
Replacer CHARS_REPLACER = new CharsReplacer(Replacer.Closure.PERCENT);
Replacer OLD_CHARS_REPLACER = new OldCharsReplacer(Replacer.Closure.PERCENT);
final class MockPlayerPlaceholderExpansion extends PlaceholderExpansion {
public static final String PLAYER_X = "10";
public static final String PLAYER_Y = "20";
public static final String PLAYER_Z = "30";
public static final String PLAYER_NAME = "Sxtanna";
@NotNull
@Override
public String getIdentifier() {
return "player";
}
@NotNull
@Override
public String getAuthor() {
return "Sxtanna";
}
@NotNull
@Override
public String getVersion() {
return "1.0";
}
@Override
public String onRequest(@Nullable final OfflinePlayer player, @NotNull final String params) {
final String[] parts = params.split("_");
if (parts.length == 0) {
return null;
}
switch (parts[0]) {
case "name":
return PLAYER_NAME;
case "x":
return PLAYER_X;
case "y":
return PLAYER_Y;
case "z":
return PLAYER_Z;
}
return null;
}
}
}
@@ -0,0 +1,136 @@
package me.clip.placeholderapi.replacer;
/*
* This file is part of PlaceholderAPI
*
* PlaceholderAPI
* Copyright (c) 2015 - 2026 PlaceholderAPI Team
*
* PlaceholderAPI free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlaceholderAPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import java.util.Locale;
import java.util.function.Function;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class OldCharsReplacer implements Replacer {
@NotNull
private final Closure closure;
public OldCharsReplacer(@NotNull final Closure closure) {
this.closure = closure;
}
@NotNull
@Override
public String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
@NotNull final Function<String, @Nullable PlaceholderExpansion> lookup) {
final char[] chars = text.toCharArray();
final StringBuilder builder = new StringBuilder(text.length());
final StringBuilder identifier = new StringBuilder();
final StringBuilder parameters = new StringBuilder();
for (int i = 0; i < chars.length; i++) {
final char l = chars[i];
if (l != closure.head || i + 1 >= chars.length) {
builder.append(l);
continue;
}
boolean identified = false;
boolean invalid = true;
boolean hadSpace = false;
while (++i < chars.length) {
final char p = chars[i];
if (p == ' ' && !identified) {
hadSpace = true;
break;
}
if (p == closure.tail) {
invalid = false;
break;
}
if (p == '_' && !identified) {
identified = true;
continue;
}
if (identified) {
parameters.append(p);
} else {
identifier.append(p);
}
}
final String identifierString = identifier.toString();
final String lowercaseIdentifierString = identifierString.toLowerCase(Locale.ROOT);
final String parametersString = parameters.toString();
identifier.setLength(0);
parameters.setLength(0);
if (invalid) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_').append(parametersString);
}
if (hadSpace) {
builder.append(' ');
}
continue;
}
final PlaceholderExpansion placeholder = lookup.apply(lowercaseIdentifierString);
if (placeholder == null) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_');
}
builder.append(parametersString).append(closure.tail);
continue;
}
final String replacement = placeholder.onRequest(player, parametersString);
if (replacement == null) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_');
}
builder.append(parametersString).append(closure.tail);
continue;
}
builder.append(replacement);
}
return builder.toString();
}
}
@@ -0,0 +1,66 @@
/*
* This file is part of PlaceholderAPI
*
* PlaceholderAPI
* Copyright (c) 2015 - 2026 PlaceholderAPI Team
*
* PlaceholderAPI free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlaceholderAPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.clip.placeholderapi.replacer;
import me.clip.placeholderapi.Values;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.jetbrains.annotations.Nullable;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode({Mode.AverageTime, Mode.Throughput})
@Fork(value = 3, warmups = 1)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
public class ReplacerBenchmarks {
private Function<String, @Nullable PlaceholderExpansion> expansionFunction;
@Setup
public void setup() {
this.expansionFunction = Values.PLACEHOLDERS::get;
}
@Benchmark
public void measureCharsReplacerSmallText(final Blackhole blackhole) {
blackhole.consume(Values.CHARS_REPLACER.apply(Values.SMALL_TEXT, null, expansionFunction));
}
@Benchmark
public void measureCharsReplacerLargeText(final Blackhole blackhole) {
blackhole.consume(Values.CHARS_REPLACER.apply(Values.LARGE_TEXT, null, expansionFunction));
}
@Benchmark
public void measureCharsReplacerSmallTextOld(final Blackhole blackhole) {
blackhole.consume(Values.OLD_CHARS_REPLACER.apply(Values.SMALL_TEXT, null, expansionFunction));
}
@Benchmark
public void measureCharsReplacerLargeTextOld(final Blackhole blackhole) {
blackhole.consume(Values.OLD_CHARS_REPLACER.apply(Values.LARGE_TEXT, null, expansionFunction));
}
}
@@ -20,12 +20,7 @@
package me.clip.placeholderapi;
import com.google.common.collect.ImmutableSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -85,7 +80,11 @@ public final class PlaceholderAPI {
@NotNull
public static List<String> setPlaceholders(final OfflinePlayer player,
@NotNull final List<String> text) {
return text.stream().map(line -> setPlaceholders(player, line)).collect(Collectors.toList());
final List<String> result = new ArrayList<>(text.size());
for (final String line : text) {
result.add(setPlaceholders(player, line));
}
return result;
}
/**
@@ -140,8 +139,11 @@ public final class PlaceholderAPI {
@NotNull
public static List<@NotNull String> setBracketPlaceholders(final OfflinePlayer player,
@NotNull final List<@NotNull String> text) {
return text.stream().map(line -> setBracketPlaceholders(player, line))
.collect(Collectors.toList());
final List<String> result = new ArrayList<>(text.size());
for (final String line : text) {
result.add(setBracketPlaceholders(player, line));
}
return result;
}
/**
@@ -179,14 +181,14 @@ public final class PlaceholderAPI {
* @param text Text to parse the placeholders in
* @return The text containing the parsed relational placeholders
*/
public static String setRelationalPlaceholders(Player one, Player two, String text) {
public static String setRelationalPlaceholders(final Player one, final Player two, @NotNull String text) {
final Matcher matcher = RELATIONAL_PLACEHOLDER_PATTERN.matcher(text);
while (matcher.find()) {
final String format = matcher.group(2);
final int index = format.indexOf("_");
final int index = format.indexOf('_');
if (index <= 0 || index >= format.length()) {
if (index <= 0) {
continue;
}
@@ -218,9 +220,12 @@ public final class PlaceholderAPI {
* @param text text to parse the placeholder values to
* @return The text containing the parsed relational placeholders
*/
public static List<String> setRelationalPlaceholders(Player one, Player two, List<String> text) {
return text.stream().map(line -> setRelationalPlaceholders(one, two, line))
.collect(Collectors.toList());
public static List<String> setRelationalPlaceholders(final Player one, final Player two, final @NotNull List<String> text) {
final List<String> result = new ArrayList<>(text.size());
for (final String line : text) {
result.add(setRelationalPlaceholders(one, two, line));
}
return result;
}
/**
@@ -241,8 +246,7 @@ public final class PlaceholderAPI {
*/
@NotNull
public static Set<String> getRegisteredIdentifiers() {
return ImmutableSet
.copyOf(PlaceholderAPIPlugin.getInstance().getLocalExpansionManager().getIdentifiers());
return PlaceholderAPIPlugin.getInstance().getLocalExpansionManager().getIdentifiers();
}
/**
@@ -279,8 +283,15 @@ public final class PlaceholderAPI {
* @param text String to check
* @return true if String contains any matches to the normal placeholder pattern, false otherwise
*/
public static boolean containsPlaceholders(String text) {
return text != null && PLACEHOLDER_PATTERN.matcher(text).find();
public static boolean containsPlaceholders(final String text) {
if (text == null) {
return false;
}
final int firstPercent = text.indexOf('%');
if (firstPercent == -1) {
return false;
}
return text.indexOf('%', firstPercent + 1) != -1;
}
/**
@@ -290,8 +301,15 @@ public final class PlaceholderAPI {
* @param text String to check
* @return true if String contains any matches to the bracket placeholder pattern, false otherwise
*/
public static boolean containsBracketPlaceholders(String text) {
return text != null && BRACKET_PLACEHOLDER_PATTERN.matcher(text).find();
public static boolean containsBracketPlaceholders(final String text) {
if (text == null) {
return false;
}
final int openBracket = text.indexOf('{');
if (openBracket == -1) {
return false;
}
return text.indexOf('}', openBracket + 1) != -1;
}
// === Deprecated API ===
@@ -60,17 +60,8 @@ public final class PlaceholderAPIPlugin extends JavaPlugin {
private static PlaceholderAPIPlugin instance;
static {
String version = Bukkit.getServer().getBukkitVersion().split("-")[0];
String suffix;
if (version.chars()
.filter(c -> c == '.')
.count() == 1) {
suffix = "R1";
version = 'v' + version.replace('.', '_') + '_' + suffix;
} else {
int minor = Integer.parseInt(version.split("\\.")[2].charAt(0) + "");
version = 'v' + version.replace('.', '_').replace("_" + minor, "") + '_' + "R" + (minor - 1);
}
final String version = ServerVersionResolver.resolve(
Bukkit.getServer().getBukkitVersion()).getLegacyVersion();
boolean isSpigot;
try {
@@ -0,0 +1,133 @@
/*
* This file is part of PlaceholderAPI
*
* PlaceholderAPI
* Copyright (c) 2015 - 2026 PlaceholderAPI Team
*
* PlaceholderAPI free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlaceholderAPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.clip.placeholderapi;
import java.util.OptionalInt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
final class ServerVersionResolver {
private static final String PAPER_BUILD_INFO_PROVIDER =
"me.clip.placeholderapi.PaperServerBuildInfoProvider";
private ServerVersionResolver() {
}
@NotNull
static ServerBuild resolve(@NotNull final String bukkitVersion) {
return resolve(bukkitVersion, resolvePaperBuildInfo());
}
@NotNull
static ServerBuild resolve(@NotNull final String bukkitVersion,
@Nullable final ServerBuild paperBuildInfo) {
if (paperBuildInfo != null) {
return paperBuildInfo;
}
return new ServerBuild(normalizeBukkitVersion(bukkitVersion), OptionalInt.empty());
}
@Nullable
private static ServerBuild resolvePaperBuildInfo() {
try {
final Class<?> provider = Class.forName(
PAPER_BUILD_INFO_PROVIDER,
true,
ServerVersionResolver.class.getClassLoader()
);
final Object result = provider.getMethod("get").invoke(null);
if (result instanceof ServerBuild) {
return (ServerBuild) result;
}
} catch (final ReflectiveOperationException | LinkageError ignored) {
// Paper is unavailable or exposes an incompatible ServerBuildInfo API.
}
return null;
}
@NotNull
static String normalizeBukkitVersion(@NotNull final String bukkitVersion) {
String version = bukkitVersion.split("-", 2)[0];
// Some Paper development versions include build metadata in the Bukkit version.
// Keep this fallback for Paper forks that do not expose ServerBuildInfo.
final int paperBuildMetadataIndex = version.indexOf(".build.");
if (paperBuildMetadataIndex != -1) {
version = version.substring(0, paperBuildMetadataIndex);
}
return version;
}
static final class ServerBuild {
@NotNull
private final String minecraftVersionId;
@NotNull
private final OptionalInt buildNumber;
ServerBuild(@NotNull final String minecraftVersionId,
@NotNull final OptionalInt buildNumber) {
this.minecraftVersionId = minecraftVersionId;
this.buildNumber = buildNumber;
}
@NotNull
String getMinecraftVersionId() {
return minecraftVersionId;
}
@NotNull
OptionalInt getBuildNumber() {
return buildNumber;
}
@NotNull
String getLegacyVersion() {
String version = minecraftVersionId;
if (version.chars()
.filter(c -> c == '.')
.count() == 1) {
return 'v' + version.replace('.', '_') + "_R1";
}
final String[] versionParts = version.split("\\.");
int minor = 1;
if (versionParts.length > 2 && !versionParts[2].isEmpty()) {
try {
minor = Integer.parseInt(versionParts[2].charAt(0) + "");
} catch (final NumberFormatException ignored) {
minor = 1;
}
}
return 'v' + version.replace('.', '_').replace("_" + minor, "")
+ "_R" + (minor - 1);
}
}
}
@@ -32,6 +32,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.stream.Stream;
import com.google.common.collect.Lists;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.commands.impl.cloud.CommandECloud;
import me.clip.placeholderapi.commands.impl.local.CommandDump;
@@ -119,14 +120,18 @@ public final class PlaceholderCommandRouter implements CommandExecutor, TabCompl
}
@Override
public List<String> onTabComplete(@NotNull final CommandSender sender,
@NotNull final Command command, @NotNull final String alias, @NotNull final String[] args) {
public List<String> onTabComplete(@NotNull final CommandSender sender, @NotNull final Command command,
@NotNull final String alias, @NotNull final String[] args) {
final List<String> suggestions = new ArrayList<>();
if (args.length > 1) {
final PlaceholderCommand target = this.commands.get(args[0].toLowerCase(Locale.ROOT));
if (target != null) {
if (target.getPermission() != null && !target.getPermission().isEmpty() && !sender.hasPermission(target.getPermission())) {
return suggestions;
}
target.complete(plugin, sender, args[0].toLowerCase(Locale.ROOT),
Arrays.asList(Arrays.copyOfRange(args, 1, args.length)), suggestions);
}
@@ -118,6 +118,11 @@ public final class CommandECloud extends PlaceholderCommand {
return;
}
if (!target.getLabel().equalsIgnoreCase("refresh") && plugin.getCloudExpansionManager().isEmpty()) {
Msg.msg(sender, "&cThere is no available data from the eCloud. Please try running &f/papi ecloud refresh&c. If this does not resolve the issue, the eCloud may be blocked by your firewall, server host, or service provider.\n&r\n&cMore information: &fhttps://placeholderapi.com/ecloud-blocked");
return;
}
target.evaluate(plugin, sender, search, params.subList(1, params.size()));
}
@@ -170,6 +170,8 @@ public final class CommandECloudExpansionList extends PlaceholderCommand {
.append(newline()).append(newline())
.append(text("Author: ", AQUA)).append(text(expansion.getAuthor(), WHITE))
.append(newline())
.append(text("Version: ", AQUA)).append(text(expansion.getVersion().getVersion(), WHITE))
.append(newline())
.append(text("Verified: ", AQUA)).append(text(expansion.getVersion().isVerified() ? "✔" : "❌", expansion.getVersion().isVerified() ? GREEN : RED, TextDecoration.BOLD))
.append(newline())
.append(text("Released: ", AQUA)).append(text(format.format(expansion.getLastUpdate()), WHITE))
@@ -91,6 +91,8 @@ public final class CommandDump extends PlaceholderCommand {
@NotNull
private CompletableFuture<String> postDump(@NotNull final String dump) {
System.out.println("DUMP CREATE SUCCESS");
return CompletableFuture.supplyAsync(() -> {
try {
final HttpURLConnection connection = ((HttpURLConnection) new URL(URL + "documents")
@@ -110,6 +112,7 @@ public final class CommandDump extends PlaceholderCommand {
return gson.fromJson(json, JsonObject.class).get("key").getAsString();
}
} catch (final IOException ex) {
ex.printStackTrace();
throw new CompletionException(ex);
}
});
@@ -123,6 +126,8 @@ public final class CommandDump extends PlaceholderCommand {
.append(DATE_FORMAT.format(Instant.now()))
.append("\n\n");
System.out.println("DUMP1");
builder.append("PlaceholderAPI: ")
.append(plugin.getDescription().getVersion())
.append("\n\n");
@@ -130,6 +135,8 @@ public final class CommandDump extends PlaceholderCommand {
builder.append("Expansions Registered:")
.append('\n');
System.out.println("DUMP2");
final List<PlaceholderExpansion> expansions = plugin.getLocalExpansionManager()
.getExpansions()
.stream()
@@ -139,6 +146,8 @@ public final class CommandDump extends PlaceholderCommand {
)
.collect(Collectors.toList());
System.out.println("DUMP3");
int size = expansions.stream().map(e -> e.getIdentifier().length())
.max(Integer::compareTo)
.orElse(0);
@@ -163,6 +172,7 @@ public final class CommandDump extends PlaceholderCommand {
.getExpansionsFolder()
.list((dir, name) -> name.toLowerCase(Locale.ROOT).endsWith(".jar"));
System.out.println("DUMP4");
if (jars == null) {
builder.append(" ¨[Warning]: Could not load jar files from expansions folder.");
@@ -182,10 +192,14 @@ public final class CommandDump extends PlaceholderCommand {
.append(plugin.getServer().getVersion())
.append("\n");
System.out.println("DUMP5");
builder.append("Java Version: ")
.append(System.getProperty("java.version"))
.append("\n\n");
System.out.println("DUMP6");
builder.append("Plugin Info:")
.append('\n');
@@ -193,6 +207,8 @@ public final class CommandDump extends PlaceholderCommand {
.sorted(Comparator.comparing(Plugin::getName))
.collect(Collectors.toList());
System.out.println("DUMP7");
size = plugins.stream().map(pl -> pl.getName().length())
.max(Integer::compareTo)
.orElse(0);
@@ -208,6 +224,8 @@ public final class CommandDump extends PlaceholderCommand {
.append("\n");
}
System.out.println("DUMP8");
return builder.toString();
}
}
@@ -54,6 +54,10 @@ public final class PlaceholderAPIConfig {
return plugin.getConfig().getBoolean("debug", false);
}
public boolean useAdventureReplacer() {
return plugin.getConfig().getBoolean("use_adventure_provided_replacer", false);
}
public Optional<ExpansionSort> getExpansionSort() {
final String option = plugin.getConfig()
@@ -87,8 +91,11 @@ public final class PlaceholderAPIConfig {
return plugin.getConfig().getString("boolean.false", "false");
}
public boolean useAdventureProvidedReplacer() {
return plugin.getConfig().getBoolean("use_adventure_provided_replacer", false);
}
public boolean detectMaliciousExpansions() {
return plugin.getConfig().getBoolean("detect_malicious_expansions", true);
}
}
@@ -26,11 +26,9 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.*;
import java.lang.reflect.Type;
import java.net.URL;
import java.net.*;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
@@ -60,9 +58,27 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
public final class CloudExpansionManager {
@NotNull
public static final String USER_AGENT;
static {
String userAgent;
try (final InputStream in = PlaceholderAPIPlugin.class.getResourceAsStream("/user-agent.txt")) {
if (in == null) {
userAgent = "PlaceholderAPI-Bukkit-null";
} else {
userAgent = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).readLine();
}
} catch (IOException e) {
userAgent = "PlaceholderAPI-Bukkit-null";
}
USER_AGENT = userAgent;
}
@NotNull
private static final String API_URL = "https://ecloud.placeholderapi.com/api/v3/";
private static final String API_URL = "https://ecloud.placeholderapi.com/api/v3/?platform=bukkit";
@NotNull
private static final Gson GSON = new Gson();
@@ -115,6 +131,10 @@ public final class CloudExpansionManager {
return ImmutableMap.copyOf(cache);
}
public boolean isEmpty() {
return cache.isEmpty();
}
@NotNull
@Unmodifiable
public Map<String, CloudExpansion> getCloudExpansionsInstalled() {
@@ -180,8 +200,16 @@ public final class CloudExpansionManager {
// a defence tactic! use ConcurrentHashMap instead of normal HashMap
Map<String, CloudExpansion> values = new ConcurrentHashMap<>();
try {
//noinspection UnstableApiUsage
String json = Resources.toString(new URL(API_URL), StandardCharsets.UTF_8);
final URI uri = new URI(API_URL);
final URLConnection connection = uri.toURL().openConnection();
connection.setRequestProperty("User-Agent", USER_AGENT);
final String json;
try (final InputStream in = connection.getInputStream()) {
final BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
json = reader.lines().collect(Collectors.joining(System.lineSeparator()));
}
values.putAll(GSON.fromJson(json, TYPE));
List<String> toRemove = new ArrayList<>();
@@ -197,6 +225,8 @@ public final class CloudExpansionManager {
for (String name : toRemove) {
values.remove(name);
}
} catch (UnknownHostException e) {
plugin.getLogger().log(Level.WARNING, "There is no data available from the eCloud. Please try running /papi refresh. If this does not resolve the issue, the eCloud may be blocked by your firewall, server host, or service provider.\n\nMore information: https://placeholderapi.com/ecloud-blocked", e);
} catch (Throwable e) {
// ugly swallowing of every throwable, but we have to be defensive
plugin.getLogger().log(Level.WARNING, "Failed to download expansion information", e);
@@ -253,10 +283,14 @@ public final class CloudExpansionManager {
"Expansion-" + toIndexName(expansion) + ".jar");
final CompletableFuture<File> download = CompletableFuture.supplyAsync(() -> {
try (final ReadableByteChannel source = Channels.newChannel(new URL(version.getUrl())
.openStream()); final FileOutputStream target = new FileOutputStream(file)) {
target.getChannel().transferFrom(source, 0, Long.MAX_VALUE);
} catch (final IOException ex) {
try {
final URLConnection connection = new URI(version.getUrl()).toURL().openConnection();
connection.setRequestProperty("User-Agent", USER_AGENT);
try (final ReadableByteChannel source = Channels.newChannel(connection.getInputStream()); final FileOutputStream target = new FileOutputStream(file)) {
target.getChannel().transferFrom(source, 0, Long.MAX_VALUE);
}
} catch (final IOException | URISyntaxException ex) {
throw new CompletionException(ex);
}
return file;
@@ -94,7 +94,7 @@ public final class LocalExpansionManager implements Listener {
this.folder = new File(plugin.getDataFolder(), EXPANSIONS_FOLDER_NAME);
if (!this.folder.exists() && !folder.mkdirs()) {
Msg.warn("Failed to create expansions folder!");
plugin.getLogger().warning("Failed to create expansions folder!");
}
}
@@ -114,7 +114,7 @@ public final class LocalExpansionManager implements Listener {
@NotNull
@Unmodifiable
public Collection<String> getIdentifiers() {
public Set<String> getIdentifiers() {
expansionsLock.lock();
try {
return ImmutableSet.copyOf(expansions.keySet());
@@ -136,12 +136,7 @@ public final class LocalExpansionManager implements Listener {
@Nullable
public PlaceholderExpansion getExpansion(@NotNull final String identifier) {
expansionsLock.lock();
try {
return expansions.get(identifier.toLowerCase(Locale.ROOT));
} finally {
expansionsLock.unlock();
}
return expansions.get(identifier.toLowerCase(Locale.ROOT));
}
@NotNull
@@ -356,13 +351,19 @@ public final class LocalExpansionManager implements Listener {
Msg.info("Placeholder expansion registration initializing...");
Futures.onMainThread(plugin, findExpansionsOnDisk(), (classes, exception) -> {
System.out.println("LOAD1");
if (exception != null) {
Msg.severe("Failed to load class files of expansion.", exception);
exception.printStackTrace();
return;
}
System.out.println("LOAD2 - size: " + classes.size());
final List<PlaceholderExpansion> registered = classes.stream()
.filter(Objects::nonNull)
.peek(clazz -> System.out.println("Attempting register of - " + clazz))
.map(this::register)
.filter(Optional::isPresent)
.map(Optional::get)
@@ -391,6 +392,8 @@ public final class LocalExpansionManager implements Listener {
Msg.msg(sender, message.toString());
Bukkit.getPluginManager().callEvent(new ExpansionsLoadedEvent(registered));
System.out.println("LOAD3");
});
}
@@ -406,7 +409,12 @@ public final class LocalExpansionManager implements Listener {
@NotNull
public CompletableFuture<@NotNull List<@Nullable Class<? extends PlaceholderExpansion>>> findExpansionsOnDisk() {
System.out.println(folder);
File[] files = folder.listFiles((dir, name) -> name.endsWith(".jar"));
System.out.println("FIND FILES - " + Arrays.toString(files));
if (files == null) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
@@ -419,6 +427,8 @@ public final class LocalExpansionManager implements Listener {
@NotNull
public CompletableFuture<@Nullable Class<? extends PlaceholderExpansion>> findExpansionInFile(
@NotNull final File file) {
System.out.println("FOUND FILE - " + file);
return CompletableFuture.supplyAsync(() -> {
try {
final Class<? extends PlaceholderExpansion> expansionClass = FileUtil.findClass(file, PlaceholderExpansion.class);
@@ -460,7 +470,7 @@ public final class LocalExpansionManager implements Listener {
throw ((LinkageError) ex.getCause());
}
Msg.warn("There was an issue with loading an expansion.");
Msg.severe("Failed to create expansion instance for class %s.", ex, clazz.getName());
return null;
}
}
@@ -22,9 +22,7 @@ package me.clip.placeholderapi.replacer;
import java.util.Locale;
import java.util.function.Function;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -38,101 +36,116 @@ public final class CharsReplacer implements Replacer {
this.closure = closure;
}
/**
* Translates placeholders within the provided text using a high-performance
* character-scanning approach.
* * <p>The method identifies placeholders delimited by the defined {@link Closure}
* (e.g., %identifier_params% or {identifier_params}). If a placeholder is
* successfully identified, the provided lookup function is used to fetch the
* corresponding {@link PlaceholderExpansion}.</p>
*
* @param text The raw text containing potential placeholders to be replaced.
* @param player The {@link OfflinePlayer} to contextually parse the placeholders against.
* May be {@code null} if no player context is available.
* @param lookup A function that maps a lowercase identifier string to a registered
* {@link PlaceholderExpansion}.
* @return A string with all valid placeholders replaced by their respective values.
* Returns the original text if no placeholders are found.
*/
@NotNull
@Override
public String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
@NotNull final Function<String, @Nullable PlaceholderExpansion> lookup) {
final char[] chars = text.toCharArray();
final StringBuilder builder = new StringBuilder(text.length());
final char head = closure.head;
int startPlaceholder = text.indexOf(head);
final StringBuilder identifier = new StringBuilder();
final StringBuilder parameters = new StringBuilder();
if (startPlaceholder == -1) {
return text;
}
for (int i = 0; i < chars.length; i++) {
final char l = chars[i];
final int length = text.length();
final StringBuilder builder = new StringBuilder(length + (length >> 3));
int cursor = 0;
if (l != closure.head || i + 1 >= chars.length) {
builder.append(l);
final char tail = closure.tail;
loop: do {
// Append plain text preceding the placeholder
if (startPlaceholder > cursor) {
builder.append(text, cursor, startPlaceholder);
}
final int endPlaceholder = text.indexOf(tail, startPlaceholder + 1);
if (endPlaceholder == -1) {
builder.append(text, startPlaceholder, length);
return builder.toString();
}
int underscoreIndex = -1;
for (int i = startPlaceholder + 1; i < endPlaceholder; i++) {
final char current = text.charAt(i);
if (current == ' ' && underscoreIndex == -1) {
// Invalid placeholder (contains space before _).
// Treat the opening symbol as literal text and search for the next one.
builder.append(head);
cursor = startPlaceholder + 1;
startPlaceholder = text.indexOf(head, cursor);
// Safety check: If no more placeholders exist, break to finalize
if (startPlaceholder == -1) {
break loop;
}
continue loop;
}
if (current == '_' && underscoreIndex == -1) {
underscoreIndex = i;
}
}
if (underscoreIndex == -1) {
builder.append(text, startPlaceholder, endPlaceholder + 1);
cursor = endPlaceholder + 1;
startPlaceholder = text.indexOf(head, cursor);
continue;
}
boolean identified = false;
boolean invalid = true;
boolean hadSpace = false;
String identifier = text.substring(startPlaceholder + 1, underscoreIndex);
String parameters = "";
while (++i < chars.length) {
final char p = chars[i];
if (p == ' ' && !identified) {
hadSpace = true;
break;
}
if (p == closure.tail) {
invalid = false;
break;
}
if (p == '_' && !identified) {
identified = true;
continue;
}
if (identified) {
parameters.append(p);
} else {
identifier.append(p);
}
if (underscoreIndex + 1 < endPlaceholder) {
parameters = text.substring(underscoreIndex + 1, endPlaceholder);
}
final String identifierString = identifier.toString();
final String lowercaseIdentifierString = identifierString.toLowerCase(Locale.ROOT);
final String parametersString = parameters.toString();
final PlaceholderExpansion expansion = lookup.apply(identifier.toLowerCase(Locale.ROOT));
String replacement = null;
identifier.setLength(0);
parameters.setLength(0);
if (invalid) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_').append(parametersString);
}
if (hadSpace) {
builder.append(' ');
}
continue;
if (expansion != null) {
replacement = expansion.onRequest(player, parameters);
}
final PlaceholderExpansion placeholder = lookup.apply(lowercaseIdentifierString);
if (placeholder == null) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_');
}
builder.append(parametersString).append(closure.tail);
continue;
if (replacement != null) {
builder.append(replacement);
} else {
// Fallback: Restore original placeholder format
builder.append(head).append(identifier);
builder.append('_').append(parameters);
builder.append(tail);
}
final String replacement = placeholder.onRequest(player, parametersString);
if (replacement == null) {
builder.append(closure.head).append(identifierString);
cursor = endPlaceholder + 1;
startPlaceholder = text.indexOf(head, cursor);
if (identified) {
builder.append('_');
}
} while (startPlaceholder != -1);
builder.append(parametersString).append(closure.tail);
continue;
}
builder.append(replacement);
if (cursor < length) {
builder.append(text, cursor, length);
}
return builder.toString();
}
}
}
@@ -26,6 +26,8 @@ import java.net.URL;
import java.util.Arrays;
import javax.net.ssl.HttpsURLConnection;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.scheduler.scheduling.schedulers.TaskScheduler;
import me.clip.placeholderapi.util.Msg;
@@ -36,12 +38,13 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class UpdateChecker implements Listener {
private static final String MODRINTH_URL = "https://api.modrinth.com/v2/project/lKEzGugV/version";
private static final int RESOURCE_ID = 6245;
private final PlaceholderAPIPlugin plugin;
private final TaskScheduler scheduler;
private final String pluginVersion;
private String spigotVersion;
private String modrinthVersion;
private boolean updateAvailable;
public UpdateChecker(PlaceholderAPIPlugin plugin) {
@@ -54,27 +57,27 @@ public class UpdateChecker implements Listener {
return updateAvailable;
}
public String getSpigotVersion() {
return spigotVersion;
public String getModrinthVersion() {
return modrinthVersion;
}
public void fetch() {
scheduler.runTaskAsynchronously(() -> {
try {
HttpsURLConnection con = (HttpsURLConnection) new URL(
"https://api.spigotmc.org/legacy/update.php?resource=" + RESOURCE_ID).openConnection();
HttpsURLConnection con = (HttpsURLConnection) new URL(MODRINTH_URL).openConnection();
con.setRequestMethod("GET");
spigotVersion = new BufferedReader(new InputStreamReader(con.getInputStream())).readLine();
final JsonElement json = new Gson().fromJson(new BufferedReader(new InputStreamReader(con.getInputStream())), JsonElement.class);
modrinthVersion = json.getAsJsonArray().get(0).getAsJsonObject().get("version_number").getAsString();
} catch (Exception ex) {
plugin.getLogger().info("Failed to check for updates on spigot.");
plugin.getLogger().info("Failed to check for updates on modrinth.");
return;
}
if (spigotVersion == null || spigotVersion.isEmpty()) {
if (modrinthVersion == null || modrinthVersion.isEmpty()) {
return;
}
updateAvailable = spigotIsNewer();
updateAvailable = modrinthIsNewer();
if (!updateAvailable) {
return;
@@ -82,21 +85,21 @@ public class UpdateChecker implements Listener {
scheduler.runTask(() -> {
plugin.getLogger()
.info("An update for PlaceholderAPI (v" + getSpigotVersion() + ") is available at:");
.info("An update for PlaceholderAPI (v" + getModrinthVersion() + ") is available at:");
plugin.getLogger()
.info("https://www.spigotmc.org/resources/placeholderapi." + RESOURCE_ID + "/");
.info("https://modrinth.com/plugin/placeholderapi");
Bukkit.getPluginManager().registerEvents(this, plugin);
});
});
}
private boolean spigotIsNewer() {
if (spigotVersion == null || spigotVersion.isEmpty()) {
private boolean modrinthIsNewer() {
if (modrinthVersion == null || modrinthVersion.isEmpty()) {
return false;
}
int[] plV = toReadable(pluginVersion);
int[] spV = toReadable(spigotVersion);
int[] spV = toReadable(modrinthVersion);
if (plV[0] < spV[0]) {
return true;
@@ -119,10 +122,9 @@ public class UpdateChecker implements Listener {
public void onJoin(PlayerJoinEvent e) {
if (e.getPlayer().hasPermission("placeholderapi.updatenotify")) {
Msg.msg(e.getPlayer(),
"&bAn update for &fPlaceholder&7API &e(&fPlaceholder&7API &fv" + getSpigotVersion()
"&bAn update for &fPlaceholder&7API &e(&fPlaceholder&7API &fv" + getModrinthVersion()
+ "&e)"
, "&bis available at &ehttps://www.spigotmc.org/resources/placeholderapi." + RESOURCE_ID
+ "/");
, "&bis available at &ehttps://modrinth.com/plugin/placeholderapi");
}
}
}
@@ -4,10 +4,15 @@ import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.expansion.manager.CloudExpansionManager;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
@@ -47,7 +52,10 @@ public final class ExpansionSafetyCheck {
final Set<String> knownMaliciousExpansions;
try {
final String hashes = Resources.toString(new URL("https://check.placeholderapi.com"), StandardCharsets.UTF_8);
final URLConnection connection = new URI("https://check.placeholderapi.com").toURL().openConnection();
connection.setRequestProperty("User-Agent", CloudExpansionManager.USER_AGENT);
final String hashes = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))
.lines().collect(Collectors.joining(System.lineSeparator()));
knownMaliciousExpansions = Arrays.stream(hashes.split("\n")).collect(Collectors.toSet());
} catch (Exception e) {
main.getLogger().log(Level.SEVERE, "Failed to download anti malware hash check list from https://check.placeholderapi.com", e);
@@ -55,9 +63,18 @@ public final class ExpansionSafetyCheck {
}
final Set<String> maliciousPaths = new HashSet<>();
final File[] files = expansionsFolder.listFiles();
for (File file : expansionsFolder.listFiles()) {
if (files == null) {
return false;
}
for (File file : files) {
try {
if (!file.isFile()) {
continue;
}
final String hash = Hashing.sha256().hashBytes(Files.asByteSource(file).read()).toString();
if (knownMaliciousExpansions.contains(hash)) {
+3
View File
@@ -6,6 +6,8 @@
# Expansions: https://placeholderapi.com/ecloud
# Wiki: https://wiki.placeholderapi.com/
# Discord: https://helpch.at/discord
# If you're seeing performance issues with plugins that use component replacement, or things don't look quite right,
# switch to the replacer provided by adventure itself by changing use_adventure_provided_replacer to true
# No placeholders are provided with this plugin by default.
# Download placeholders: /papi ecloud
check_updates: true
@@ -16,4 +18,5 @@ boolean:
'false': 'no'
date_format: MM/dd/yy HH:mm:ss
detect_malicious_expansions: true
use_adventure_provided_replacer: false
debug: false
+1
View File
@@ -0,0 +1 @@
PlaceholderAPI-Bukkit-${version}
@@ -20,15 +20,19 @@
package me.clip.placeholderapi;
import me.clip.placeholderapi.replacer.ComponentReplacer;
import me.clip.placeholderapi.replacer.ExactReplacer;
import me.clip.placeholderapi.replacer.RelationalExactReplacer;
import me.clip.placeholderapi.replacer.Replacer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static me.clip.placeholderapi.PlaceholderAPI.RELATIONAL_PLACEHOLDER_PATTERN;
@@ -42,119 +46,241 @@ public final class PAPIComponents {
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setPlaceholders(final OfflinePlayer player, @NotNull final Component component) {
// TODO: explore a custom TextReplacementRenderer which doesn't use regex for performance benefits i.e. merge CharsReplacer with kyori TextReplacementRenderer
return component.replaceText(config -> config.match(PlaceholderAPI.PLACEHOLDER_PATTERN).replacement((result, builder) ->
builder.content(PERCENT_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
return setPlaceholders(player, component, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setPlaceholders(final OfflinePlayer player, @NotNull final Component component, @Nullable Function<String, ComponentLike> deserializer) {
if (PlaceholderAPIPlugin.getInstance().getPlaceholderAPIConfig().useAdventureProvidedReplacer()) {
return component.replaceText(config -> config.match(PlaceholderAPI.PLACEHOLDER_PATTERN).replacement((result, builder) -> {
String parsed = PERCENT_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion);
return deserializer == null ? builder.content(parsed) : deserializer.apply(parsed);
}));
}
return ComponentReplacer.replace(component, str -> PlaceholderAPI.setPlaceholders(player, str), deserializer == null ? null : s -> deserializer.apply(s).asComponent());
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components) {
return components.stream().map(component -> setPlaceholders(player, component)).collect(Collectors.toList());
return setPlaceholders(player, components, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components, @Nullable Function<String, ComponentLike> deserializer) {
return components.stream().map(component -> setPlaceholders(player, component, deserializer == null ? null : s -> deserializer.apply(s).asComponent())).collect(Collectors.toList());
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setPlaceholders(final Player player, @NotNull final Component component) {
return setPlaceholders((OfflinePlayer) player, component);
return setPlaceholders(player, component, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setPlaceholders(final Player player, @NotNull final Component component, @Nullable Function<String, ComponentLike> deserializer) {
return setPlaceholders((OfflinePlayer) player, component, deserializer);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @return List of components containing all translated placeholders
*/
@NotNull
public static List<Component> setPlaceholders(final Player player, @NotNull final List<Component> components) {
return setPlaceholders((OfflinePlayer) player, components);
return setPlaceholders(player, components, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return List of components containing all translated placeholders
*/
@NotNull
public static List<Component> setPlaceholders(final Player player, @NotNull final List<Component> components, @Nullable Function<String, ComponentLike> deserializer) {
return setPlaceholders((OfflinePlayer) player, components, deserializer);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setBracketPlaceholders(final OfflinePlayer player, @NotNull final Component component) {
return component.replaceText(config -> config.match(PlaceholderAPI.BRACKET_PLACEHOLDER_PATTERN).replacement((result, builder) ->
builder.content(BRACKET_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
return setBracketPlaceholders(player, component, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setBracketPlaceholders(final OfflinePlayer player, @NotNull final Component component, @Nullable Function<String, ComponentLike> deserializer) {
if (PlaceholderAPIPlugin.getInstance().getPlaceholderAPIConfig().useAdventureReplacer()) {
return component.replaceText(config -> config.match(PlaceholderAPI.BRACKET_PLACEHOLDER_PATTERN).replacement((result, builder) ->
builder.content(BRACKET_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
}
return ComponentReplacer.replace(component, str -> PlaceholderAPI.setBracketPlaceholders(player, str), deserializer == null ? null : s -> deserializer.apply(s).asComponent());
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setBracketPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components) {
return components.stream().map(component -> setBracketPlaceholders(player, component)).collect(Collectors.toList());
return setBracketPlaceholders(player, components, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setBracketPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components, @Nullable Function<String, ComponentLike> deserializer) {
return components.stream().map(component -> setBracketPlaceholders(player, component, deserializer)).collect(Collectors.toList());
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setBracketPlaceholders(final Player player, @NotNull final Component component) {
return setBracketPlaceholders((OfflinePlayer) player, component);
return setBracketPlaceholders(player, component, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setBracketPlaceholders(final Player player, @NotNull final Component component, @Nullable Function<String, ComponentLike> deserializer) {
return setBracketPlaceholders((OfflinePlayer) player, component, deserializer);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setBracketPlaceholders(final Player player, @NotNull final List<Component> components) {
return setBracketPlaceholders((OfflinePlayer) player, components);
return setBracketPlaceholders(player, components, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setBracketPlaceholders(final Player player, @NotNull final List<Component> components, @Nullable Function<String, ComponentLike> deserializer) {
return setBracketPlaceholders((OfflinePlayer) player, components, deserializer);
}
/**
* set relational placeholders in the text specified placeholders are matched with the pattern
* {@literal %<rel_(identifier)_(params)>%} when set with this method
*
* @param one First player to compare
* @param two Second player to compare
* @param one First player to compare
* @param two Second player to compare
* @param component Component to parse the placeholders in
* @return The Component containing the parsed relational placeholders
*/
public static Component setRelationalPlaceholders(Player one, Player two, Component component) {
//todo: custom replacer
return component.replaceText(config -> config.match(RELATIONAL_PLACEHOLDER_PATTERN).replacement((result, builder) ->
builder.content(RELATIONAL_EXACT_REPLACER.apply(result.group(2), one, two, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
}
@@ -163,8 +289,8 @@ public final class PAPIComponents {
* Translate placeholders in the provided List based on the relation of the two provided players.
* <br>The pattern of a valid placeholder is {@literal %rel_<identifier>_<param>%}.
*
* @param one Player to compare
* @param two Player to compare
* @param one Player to compare
* @param two Player to compare
* @param components List of Components to parse the placeholder values to
* @return The List of Components containing the parsed relational placeholders
*/
@@ -172,9 +298,4 @@ public final class PAPIComponents {
return components.stream().map(line -> setRelationalPlaceholders(one, two, line))
.collect(Collectors.toList());
}
// kyori doesn't seem to have a method that can do a contains with regex, we don't want to do a more expensive replace
// public static boolean containsPlaceholders(@Nullable final Component text) {
// return text != null && text.replaceText()
// }
}
}
@@ -18,21 +18,25 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.clip.placeholderapi.replacer;
package me.clip.placeholderapi;
import me.clip.placeholderapi.Values;
import org.openjdk.jmh.annotations.Benchmark;
import io.papermc.paper.ServerBuildInfo;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
public class ReplacerBenchmarks {
@ApiStatus.Internal
public final class PaperServerBuildInfoProvider {
@Benchmark
public void measureCharsReplacerSmallText() {
Values.CHARS_REPLACER.apply(Values.SMALL_TEXT, null, Values.PLACEHOLDERS::get);
private PaperServerBuildInfoProvider() {
}
@Benchmark
public void measureCharsReplacerLargeText() {
Values.CHARS_REPLACER.apply(Values.LARGE_TEXT, null, Values.PLACEHOLDERS::get);
}
@NotNull
public static ServerVersionResolver.ServerBuild get() {
final ServerBuildInfo buildInfo = ServerBuildInfo.buildInfo();
}
return new ServerVersionResolver.ServerBuild(
buildInfo.minecraftVersionId(),
buildInfo.buildNumber()
);
}
}
@@ -0,0 +1,175 @@
package me.clip.placeholderapi.replacer;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.api.BinaryTagHolder;
import net.kyori.adventure.text.*;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.DataComponentValue;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.Style;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class ComponentReplacer {
@NotNull
public static Component replace(@NotNull final Component component, @NotNull final Function<String, String> replacer, @Nullable final Function<String, Component> deserializer) {
return rebuild(component, replacer, deserializer);
}
@NotNull
private static Component rebuild(@NotNull final Component component, @NotNull final Function<String, String> replacer, @Nullable final Function<String, Component> deserializer) {
Component rebuilt;
if (component instanceof TextComponent) {
final TextComponent text = (TextComponent) component;
final String replaced = replacer.apply(text.content());
rebuilt = deserializer == null ? Component.text(replaced) : deserializer.apply(replaced);
} else if (component instanceof TranslatableComponent) {
final TranslatableComponent translatable = (TranslatableComponent) component;
final List<Component> arguments = new ArrayList<>();
for (final ComponentLike arg : translatable.arguments()) {
arguments.add(rebuild(arg.asComponent(), replacer, deserializer));
}
rebuilt = Component.translatable(translatable.key(), arguments);
} else if (component instanceof KeybindComponent) {
final KeybindComponent keybind = (KeybindComponent) component;
rebuilt = Component.keybind(keybind.keybind());
} else if (component instanceof ScoreComponent) {
final ScoreComponent score = (ScoreComponent) component;
rebuilt = Component.score(score.name(), score.objective());
} else if (component instanceof SelectorComponent) {
final SelectorComponent selector = (SelectorComponent) component;
rebuilt = Component.selector(selector.pattern());
} else {
rebuilt = Component.empty();
}
rebuilt = rebuilt.style(rebuildStyle(component.style(), replacer, deserializer));
if (!component.children().isEmpty()) {
final List<Component> children = new ArrayList<>();
for (Component child : component.children()) {
children.add(rebuild(child, replacer, deserializer));
}
rebuilt = rebuilt.children(children);
}
return rebuilt;
}
@NotNull
private static Style rebuildStyle(@NotNull final Style style, @NotNull final Function<String, String> replacer, @Nullable final Function<String, Component> deserializer) {
final Style.Builder builder = style.toBuilder();
final ClickEvent click = style.clickEvent();
if (click != null) {
builder.clickEvent(rebuildClickEvent(click, replacer));
}
final HoverEvent<?> hover = style.hoverEvent();
if (hover != null) {
builder.hoverEvent(rebuildHoverEvent(hover, replacer, deserializer));
}
return builder.build();
}
@NotNull
private static ClickEvent rebuildClickEvent(@NotNull final ClickEvent click, @NotNull final Function<String, String> replacer) {
final ClickEvent.Payload payload = click.payload();
if (!(payload instanceof ClickEvent.Payload.Text)) {
return click;
}
final String original = ((ClickEvent.Payload.Text) payload).value();
final String replaced = replacer.apply(original);
final ClickEvent.Action action = click.action();
switch (action) {
case OPEN_URL:
return ClickEvent.openUrl(replaced);
case OPEN_FILE:
return ClickEvent.openFile(replaced);
case RUN_COMMAND:
return ClickEvent.runCommand(replaced);
case SUGGEST_COMMAND:
return ClickEvent.suggestCommand(replaced);
case COPY_TO_CLIPBOARD:
return ClickEvent.copyToClipboard(replaced);
default:
return click;
}
}
@NotNull
private static HoverEvent<?> rebuildHoverEvent(@NotNull final HoverEvent<?> hover, @NotNull final Function<String, String> replacer, @Nullable final Function<String, Component> deserializer) {
final Object value = hover.value();
if (value instanceof Component) {
final Component rebuilt = rebuild((Component) value, replacer, deserializer);
return HoverEvent.showText(rebuilt);
}
if (value instanceof HoverEvent.ShowItem) {
return rebuildShowItem((HoverEvent.ShowItem) value, replacer);
}
if (value instanceof HoverEvent.ShowEntity) {
final HoverEvent.ShowEntity entity = (HoverEvent.ShowEntity) value;
Component rebuiltName = null;
if (entity.name() != null) {
rebuiltName = rebuild(entity.name(), replacer, deserializer);
}
return HoverEvent.showEntity(entity.type(), entity.id(), rebuiltName);
}
return hover;
}
@NotNull
private static HoverEvent<?> rebuildShowItem(@NotNull final HoverEvent.ShowItem item, @NotNull final Function<String, String> replacer) {
final BinaryTagHolder nbt = item.nbt();
if (nbt != null && !nbt.string().isEmpty()) {
final String replaced = replacer.apply(nbt.string());
return HoverEvent.showItem(item.item(), item.count(), BinaryTagHolder.binaryTagHolder(replaced));
}
//I'm not 100% sure this is how we're meant to support data components but let's give it a go and see if it causes any issues :)
final Map<Key, DataComponentValue> components = item.dataComponents();
if (!components.isEmpty()) {
final Map<Key, DataComponentValue> rebuilt = new HashMap<>();
for (final Map.Entry<Key, DataComponentValue> entry : components.entrySet()) {
final DataComponentValue value = entry.getValue();
if (!(value instanceof BinaryTagHolder)) {
rebuilt.put(entry.getKey(), value);
continue;
}
rebuilt.put(entry.getKey(), BinaryTagHolder.binaryTagHolder(replacer.apply(((BinaryTagHolder) value).string())));
}
return HoverEvent.showItem(item.item(), item.count(), rebuilt);
}
return HoverEvent.showItem(item);
}
}
@@ -0,0 +1,64 @@
/*
* This file is part of PlaceholderAPI
*
* PlaceholderAPI
* Copyright (c) 2015 - 2026 PlaceholderAPI Team
*
* PlaceholderAPI free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlaceholderAPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.clip.placeholderapi;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import java.util.OptionalInt;
import org.junit.jupiter.api.Test;
public final class ServerVersionResolverTest {
@Test
void usesPaperBuildInfoWhenAvailable() {
final ServerVersionResolver.ServerBuild paperBuild =
new ServerVersionResolver.ServerBuild("26.2", OptionalInt.of(10));
final ServerVersionResolver.ServerBuild result =
ServerVersionResolver.resolve("26.2.build.10-alpha", paperBuild);
assertSame(paperBuild, result);
assertEquals("26.2", result.getMinecraftVersionId());
assertEquals(OptionalInt.of(10), result.getBuildNumber());
assertEquals("v26_2_R1", result.getLegacyVersion());
}
@Test
void stripsPaperBuildMetadataWhenServerBuildInfoIsUnavailable() {
final ServerVersionResolver.ServerBuild result =
ServerVersionResolver.resolve("26.2.build.10-alpha", null);
assertEquals("26.2", result.getMinecraftVersionId());
assertEquals(OptionalInt.empty(), result.getBuildNumber());
assertEquals("v26_2_R1", result.getLegacyVersion());
}
@Test
void preservesTraditionalBukkitVersions() {
final ServerVersionResolver.ServerBuild result =
ServerVersionResolver.resolve("1.21.11-R0.1-SNAPSHOT", null);
assertEquals("1.21.11", result.getMinecraftVersionId());
assertEquals(OptionalInt.empty(), result.getBuildNumber());
}
}
@@ -30,6 +30,8 @@ import org.jetbrains.annotations.Nullable;
public interface Values {
String NO_ARGUMENTS_PLACEHOLDER = "%player%";
String EMPTY_ARGUMENT_PLACEHOLDER = "%player_%";
String SMALL_TEXT = "My name is %player_name%";
String LARGE_TEXT = "My name is %player_name% and my location is (%player_x%, %player_y%, %player_z%), this placeholder is invalid %server_name%";
@@ -43,6 +45,7 @@ public interface Values {
final class MockPlayerPlaceholderExpansion extends PlaceholderExpansion {
public static final String EMPTY_ARGUMENT = "Empty Argument";
public static final String PLAYER_X = "10";
public static final String PLAYER_Y = "20";
public static final String PLAYER_Z = "30";
@@ -83,6 +86,8 @@ public interface Values {
return PLAYER_Y;
case "z":
return PLAYER_Z;
case "":
return EMPTY_ARGUMENT;
}
return null;
@@ -24,6 +24,7 @@ import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.PLAYE
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.PLAYER_X;
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.PLAYER_Y;
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.PLAYER_Z;
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.EMPTY_ARGUMENT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import me.clip.placeholderapi.Values;
@@ -37,6 +38,18 @@ public final class ReplacerUnitTester {
Values.CHARS_REPLACER.apply("%player_name%", null, Values.PLACEHOLDERS::get));
}
@Test
void charsReplacersDoesNotParsePlaceholdersWithNoArguments() {
assertEquals(Values.NO_ARGUMENTS_PLACEHOLDER,
Values.CHARS_REPLACER.apply(Values.NO_ARGUMENTS_PLACEHOLDER, null, Values.PLACEHOLDERS::get));
}
@Test
void charsReplacersParsesPlaceholdersWithOneArgumentThatIsEmpty() {
assertEquals(EMPTY_ARGUMENT,
Values.CHARS_REPLACER.apply(Values.EMPTY_ARGUMENT_PLACEHOLDER, null, Values.PLACEHOLDERS::get));
}
@Test
void testCharsReplacerProducesExpectedSentence() {
assertEquals(String.format(