Compare commits

..

26 Commits

Author SHA1 Message Date
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
PiggyPiglet
880ddc22b5 bump version to 2.12.0 2026-01-19 17:20:13 +08:00
PiggyPiglet
d378f782b4 Update bstats 2026-01-19 17:11:20 +08:00
PiggyPiglet
1f2d969a69 set plain jar classifier as "" when publishing so maven actually knows what to do with it 2026-01-19 17:02:03 +08:00
PiggyPiglet
cac79a26af plainJar depends on compilePaper 2026-01-19 16:59:05 +08:00
PiggyPiglet
8b078f9058 downgrade gradle 2026-01-19 16:56:03 +08:00
PiggyPiglet
d3a5d01f55 this one's really gonna do it this time 2026-01-19 16:47:57 +08:00
PiggyPiglet
9a677d46a1 Potential gradle publish fix 2026-01-19 16:36:34 +08:00
PiggyPiglet
8116cbb385 back to one jar pls 2026-01-19 16:13:15 +08:00
PiggyPiglet
b9affd0879 Remove debug code 2026-01-18 23:09:57 +08:00
PiggyPiglet
ec8657015c jenkins attempt #2 2026-01-18 23:02:29 +08:00
PiggyPiglet
38a86e6d2d will jenkins build now idk let's see 2026-01-18 22:36:55 +08:00
PiggyPiglet
35376e43ca Merge pull request #1157 from PlaceholderAPI/feature/components
Let's have component support finally :D
2026-01-18 22:24:16 +08:00
78 changed files with 457 additions and 195 deletions

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. 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 ## 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. 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.

View File

@@ -3,103 +3,191 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins { plugins {
`java-library` `java-library`
`maven-publish` `maven-publish`
id("com.github.hierynomus.license") version "0.16.1" // id("com.github.hierynomus.license") version "0.16.1"
id("io.github.goooler.shadow") version "8.1.7" id("io.github.goooler.shadow") version "8.1.7"
} }
subprojects { group = "me.clip"
apply(plugin = "java-library") version = "2.12.2-DEV-${System.getProperty("BUILD_NUMBER")}"
apply(plugin = "maven-publish")
apply(plugin = "com.github.hierynomus.license")
apply(plugin = "io.github.goooler.shadow")
group = "me.clip" description = "An awesome placeholder provider!"
version = "2.11.8-DEV-${System.getProperty("BUILD_NUMBER")}"
description = "An awesome placeholder provider!" val paper by sourceSets.creating {
java.srcDir("src/paper/java")
repositories { // paper can see main code
maven("https://oss.sonatype.org/content/repositories/snapshots/") compileClasspath += sourceSets.main.get().output
runtimeClasspath += output + compileClasspath
}
mavenCentral() repositories {
mavenLocal() maven("https://oss.sonatype.org/content/repositories/snapshots/")
maven("https://repo.codemc.org/repository/maven-public/") mavenCentral()
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") mavenLocal()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.codemc.org/repository/maven-public/")
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
maven("https://repo.papermc.io/repository/maven-public/")
}
dependencies {
implementation("org.bstats:bstats-bukkit:3.1.0")
implementation("net.kyori:adventure-platform-bukkit:4.4.1")
add(paper.compileOnlyConfigurationName, "net.kyori:adventure-platform-bukkit:4.4.1")
add(paper.compileOnlyConfigurationName, "dev.folia:folia-api:1.21.11-R0.1-SNAPSHOT")
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")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
withJavadocJar()
withSourcesJar()
disableAutoTargetJvm()
}
val javaComponent: SoftwareComponent = components["java"]
tasks {
processResources {
eachFile { expand("version" to project.version) }
} }
dependencies { build {
implementation("org.bstats:bstats-bukkit:3.0.1") dependsOn(named("shadowJar"))
implementation("net.kyori:adventure-platform-bukkit:4.4.1")
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")
} }
register<JavaCompile>("compilePaper") {
java { source = paper.java
sourceCompatibility = JavaVersion.VERSION_1_8 classpath = paper.compileClasspath
targetCompatibility = JavaVersion.VERSION_1_8 destinationDirectory.set(layout.buildDirectory.dir("classes/java/paper"))
options.encoding = "UTF-8"
withJavadocJar() options.release = 8
withSourcesJar()
disableAutoTargetJvm()
} }
license { val plainJar by registering(Jar::class) {
header = rootProject.file("config/headers/main.txt") dependsOn("compilePaper")
include("**/*.java") archiveClassifier.set("plain")
mapping("java", "JAVADOC_STYLE") from(sourceSets.main.get().output)
from(paper.output)
}
encoding = "UTF-8" val combinedSourcesJar by registering(Jar::class) {
archiveClassifier.set("sources")
from(sourceSets.main.get().allSource)
from(paper.allSource)
ext { duplicatesStrategy = DuplicatesStrategy.EXCLUDE
set("year", 2026) }
val combinedJavadoc by registering(Javadoc::class) {
isFailOnError = false
source = sourceSets.main.get().allJava + paper.allJava
classpath = sourceSets.main.get().compileClasspath + paper.compileClasspath
with(options as StandardJavadocDocletOptions) {
addStringOption("Xdoclint:none", "-quiet")
addStringOption("encoding", "UTF-8")
addStringOption("charSet", "UTF-8")
} }
} }
tasks { val combinedJavadocJar by registering(Jar::class) {
processResources { archiveClassifier.set("javadoc")
eachFile { expand("version" to project.version) } dependsOn(combinedJavadoc)
from(combinedJavadoc.get().destinationDir)
}
withType<JavaCompile> {
options.encoding = "UTF-8"
options.release = 8
}
withType<ShadowJar> {
configurations = listOf(project.configurations.runtimeClasspath.get())
from(sourceSets.main.get().output)
archiveClassifier.set("")
relocate("org.bstats", "me.clip.placeholderapi.metrics")
relocate("net.kyori", "me.clip.placeholderapi.libs.kyori")
exclude("META-INF/versions/**")
dependsOn("compilePaper")
doLast {
val paperDir = layout.buildDirectory.dir("classes/java/paper").get().asFile
val jarFile = archiveFile.get().asFile
ant.invokeMethod("zip", mapOf(
"destfile" to jarFile,
"update" to "true",
"basedir" to paperDir
))
} }
}
build { test {
dependsOn(named("shadowJar")) useJUnitPlatform()
} }
withType<JavaCompile> { publishing {
options.encoding = "UTF-8" publications {
options.release = 8 create<MavenPublication>("maven") {
} artifactId = "placeholderapi"
withType<Javadoc> { artifact(plainJar) {
isFailOnError = false builtBy(plainJar)
classifier = ""
}
with(options as StandardJavadocDocletOptions) { artifact(combinedSourcesJar) {
addStringOption("Xdoclint:none", "-quiet") builtBy(combinedSourcesJar)
addStringOption("encoding", "UTF-8") }
addStringOption("charSet", "UTF-8")
artifact(combinedJavadocJar) {
builtBy(combinedJavadocJar)
}
} }
} }
test { repositories {
useJUnitPlatform() maven {
if ("-DEV" in version.toString()) {
url = uri("https://repo.extendedclip.com/snapshots")
} else {
url = uri("https://repo.extendedclip.com/releases")
}
credentials {
username = System.getenv("JENKINS_USER")
password = System.getenv("JENKINS_PASS")
}
}
} }
} }
configurations { publish.get().setDependsOn(listOf(build.get()))
testImplementation { }
extendsFrom(compileOnly.get())
}
}
configurations {
testImplementation {
extendsFrom(compileOnly.get())
}
} }

View File

@@ -1,44 +0,0 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
dependencies {
api(project(":spigot"))
}
val javaComponent: SoftwareComponent = components["java"]
tasks {
withType<ShadowJar> {
archiveClassifier.set("")
archiveBaseName.set("PlaceholderAPI-Paper")
relocate("org.bstats", "me.clip.placeholderapi.metrics")
exclude("META-INF/versions/**")
}
publishing {
publications {
create<MavenPublication>("maven") {
artifactId = "placeholderapi-paper"
from(javaComponent)
}
}
repositories {
maven {
if ("-DEV" in version.toString()) {
url = uri("https://repo.extendedclip.com/snapshots")
} else {
url = uri("https://repo.extendedclip.com/releases")
}
credentials {
username = System.getenv("JENKINS_USER")
password = System.getenv("JENKINS_PASS")
}
}
}
}
publish.get().setDependsOn(listOf(build.get()))
}

View File

@@ -1,3 +1 @@
rootProject.name = "PlaceholderAPI" rootProject.name = "PlaceholderAPI"
include("spigot", "paper")

View File

@@ -1,41 +0,0 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
val javaComponent: SoftwareComponent = components["java"]
tasks {
withType<ShadowJar> {
archiveClassifier.set("")
archiveBaseName.set("PlaceholderAPI-Spigot")
relocate("org.bstats", "me.clip.placeholderapi.metrics")
relocate("net.kyori", "me.clip.placeholderapi.libs.kyori")
exclude("META-INF/versions/**")
}
publishing {
publications {
create<MavenPublication>("maven") {
artifactId = "placeholderapi"
from(javaComponent)
}
}
repositories {
maven {
if ("-DEV" in version.toString()) {
url = uri("https://repo.extendedclip.com/snapshots")
} else {
url = uri("https://repo.extendedclip.com/releases")
}
credentials {
username = System.getenv("JENKINS_USER")
password = System.getenv("JENKINS_PASS")
}
}
}
}
publish.get().setDependsOn(listOf(build.get()))
}

View File

@@ -261,12 +261,6 @@ public final class PlaceholderAPIPlugin extends JavaPlugin {
final PlaceholderCommandRouter router = new PlaceholderCommandRouter(this); final PlaceholderCommandRouter router = new PlaceholderCommandRouter(this);
pluginCommand.setExecutor(router); pluginCommand.setExecutor(router);
pluginCommand.setTabCompleter(router); pluginCommand.setTabCompleter(router);
try {
getServer().getPluginManager().registerEvents((Listener) Class.forName("me.clip.placeholderapi.TestListener").newInstance(), this);
} catch (Exception e) {
e.printStackTrace();
}
} }
private void setupMetrics() { private void setupMetrics() {

View File

@@ -32,6 +32,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.google.common.collect.Lists;
import me.clip.placeholderapi.PlaceholderAPIPlugin; import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.commands.impl.cloud.CommandECloud; import me.clip.placeholderapi.commands.impl.cloud.CommandECloud;
import me.clip.placeholderapi.commands.impl.local.CommandDump; import me.clip.placeholderapi.commands.impl.local.CommandDump;
@@ -119,14 +120,18 @@ public final class PlaceholderCommandRouter implements CommandExecutor, TabCompl
} }
@Override @Override
public List<String> onTabComplete(@NotNull final CommandSender sender, public List<String> onTabComplete(@NotNull final CommandSender sender, @NotNull final Command command,
@NotNull final Command command, @NotNull final String alias, @NotNull final String[] args) { @NotNull final String alias, @NotNull final String[] args) {
final List<String> suggestions = new ArrayList<>(); final List<String> suggestions = new ArrayList<>();
if (args.length > 1) { if (args.length > 1) {
final PlaceholderCommand target = this.commands.get(args[0].toLowerCase(Locale.ROOT)); final PlaceholderCommand target = this.commands.get(args[0].toLowerCase(Locale.ROOT));
if (target != null) { 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), target.complete(plugin, sender, args[0].toLowerCase(Locale.ROOT),
Arrays.asList(Arrays.copyOfRange(args, 1, args.length)), suggestions); Arrays.asList(Arrays.copyOfRange(args, 1, args.length)), suggestions);
} }

View File

@@ -118,6 +118,11 @@ public final class CommandECloud extends PlaceholderCommand {
return; 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())); target.evaluate(plugin, sender, search, params.subList(1, params.size()));
} }

View File

@@ -170,6 +170,8 @@ public final class CommandECloudExpansionList extends PlaceholderCommand {
.append(newline()).append(newline()) .append(newline()).append(newline())
.append(text("Author: ", AQUA)).append(text(expansion.getAuthor(), WHITE)) .append(text("Author: ", AQUA)).append(text(expansion.getAuthor(), WHITE))
.append(newline()) .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(text("Verified: ", AQUA)).append(text(expansion.getVersion().isVerified() ? "" : "", expansion.getVersion().isVerified() ? GREEN : RED, TextDecoration.BOLD))
.append(newline()) .append(newline())
.append(text("Released: ", AQUA)).append(text(format.format(expansion.getLastUpdate()), WHITE)) .append(text("Released: ", AQUA)).append(text(format.format(expansion.getLastUpdate()), WHITE))

View File

@@ -54,6 +54,10 @@ public final class PlaceholderAPIConfig {
return plugin.getConfig().getBoolean("debug", false); return plugin.getConfig().getBoolean("debug", false);
} }
public boolean useAdventureReplacer() {
return plugin.getConfig().getBoolean("use_adventure_provided_replacer", false);
}
public Optional<ExpansionSort> getExpansionSort() { public Optional<ExpansionSort> getExpansionSort() {
final String option = plugin.getConfig() final String option = plugin.getConfig()
@@ -87,8 +91,11 @@ public final class PlaceholderAPIConfig {
return plugin.getConfig().getString("boolean.false", "false"); return plugin.getConfig().getString("boolean.false", "false");
} }
public boolean useAdventureProvidedReplacer() {
return plugin.getConfig().getBoolean("use_adventure_provided_replacer", false);
}
public boolean detectMaliciousExpansions() { public boolean detectMaliciousExpansions() {
return plugin.getConfig().getBoolean("detect_malicious_expansions", true); return plugin.getConfig().getBoolean("detect_malicious_expansions", true);
} }
} }

View File

@@ -31,6 +31,7 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.net.URL; import java.net.URL;
import java.net.UnknownHostException;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -62,7 +63,7 @@ import org.jetbrains.annotations.Unmodifiable;
public final class CloudExpansionManager { public final class CloudExpansionManager {
@NotNull @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 @NotNull
private static final Gson GSON = new Gson(); private static final Gson GSON = new Gson();
@@ -115,6 +116,10 @@ public final class CloudExpansionManager {
return ImmutableMap.copyOf(cache); return ImmutableMap.copyOf(cache);
} }
public boolean isEmpty() {
return cache.isEmpty();
}
@NotNull @NotNull
@Unmodifiable @Unmodifiable
public Map<String, CloudExpansion> getCloudExpansionsInstalled() { public Map<String, CloudExpansion> getCloudExpansionsInstalled() {
@@ -197,6 +202,8 @@ public final class CloudExpansionManager {
for (String name : toRemove) { for (String name : toRemove) {
values.remove(name); 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) { } catch (Throwable e) {
// ugly swallowing of every throwable, but we have to be defensive // ugly swallowing of every throwable, but we have to be defensive
plugin.getLogger().log(Level.WARNING, "Failed to download expansion information", e); plugin.getLogger().log(Level.WARNING, "Failed to download expansion information", e);

View File

@@ -26,6 +26,8 @@ import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import me.clip.placeholderapi.PlaceholderAPIPlugin; import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.scheduler.scheduling.schedulers.TaskScheduler; import me.clip.placeholderapi.scheduler.scheduling.schedulers.TaskScheduler;
import me.clip.placeholderapi.util.Msg; import me.clip.placeholderapi.util.Msg;
@@ -36,12 +38,13 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
public class UpdateChecker implements Listener { 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 static final int RESOURCE_ID = 6245;
private final PlaceholderAPIPlugin plugin; private final PlaceholderAPIPlugin plugin;
private final TaskScheduler scheduler; private final TaskScheduler scheduler;
private final String pluginVersion; private final String pluginVersion;
private String spigotVersion; private String modrinthVersion;
private boolean updateAvailable; private boolean updateAvailable;
public UpdateChecker(PlaceholderAPIPlugin plugin) { public UpdateChecker(PlaceholderAPIPlugin plugin) {
@@ -54,27 +57,27 @@ public class UpdateChecker implements Listener {
return updateAvailable; return updateAvailable;
} }
public String getSpigotVersion() { public String getModrinthVersion() {
return spigotVersion; return modrinthVersion;
} }
public void fetch() { public void fetch() {
scheduler.runTaskAsynchronously(() -> { scheduler.runTaskAsynchronously(() -> {
try { try {
HttpsURLConnection con = (HttpsURLConnection) new URL( HttpsURLConnection con = (HttpsURLConnection) new URL(MODRINTH_URL).openConnection();
"https://api.spigotmc.org/legacy/update.php?resource=" + RESOURCE_ID).openConnection();
con.setRequestMethod("GET"); con.setRequestMethod("GET");
spigotVersion = new BufferedReader(new InputStreamReader(con.getInputStream())).readLine(); final JsonElement json = JsonParser.parseReader(new BufferedReader(new InputStreamReader(con.getInputStream())));
modrinthVersion = json.getAsJsonArray().get(0).getAsJsonObject().get("version_number").getAsString();
} catch (Exception ex) { } catch (Exception ex) {
plugin.getLogger().info("Failed to check for updates on spigot."); plugin.getLogger().info("Failed to check for updates on modrinth.");
return; return;
} }
if (spigotVersion == null || spigotVersion.isEmpty()) { if (modrinthVersion == null || modrinthVersion.isEmpty()) {
return; return;
} }
updateAvailable = spigotIsNewer(); updateAvailable = modrinthIsNewer();
if (!updateAvailable) { if (!updateAvailable) {
return; return;
@@ -82,21 +85,21 @@ public class UpdateChecker implements Listener {
scheduler.runTask(() -> { scheduler.runTask(() -> {
plugin.getLogger() plugin.getLogger()
.info("An update for PlaceholderAPI (v" + getSpigotVersion() + ") is available at:"); .info("An update for PlaceholderAPI (v" + getModrinthVersion() + ") is available at:");
plugin.getLogger() plugin.getLogger()
.info("https://www.spigotmc.org/resources/placeholderapi." + RESOURCE_ID + "/"); .info("https://modrinth.com/plugin/placeholderapi");
Bukkit.getPluginManager().registerEvents(this, plugin); Bukkit.getPluginManager().registerEvents(this, plugin);
}); });
}); });
} }
private boolean spigotIsNewer() { private boolean modrinthIsNewer() {
if (spigotVersion == null || spigotVersion.isEmpty()) { if (modrinthVersion == null || modrinthVersion.isEmpty()) {
return false; return false;
} }
int[] plV = toReadable(pluginVersion); int[] plV = toReadable(pluginVersion);
int[] spV = toReadable(spigotVersion); int[] spV = toReadable(modrinthVersion);
if (plV[0] < spV[0]) { if (plV[0] < spV[0]) {
return true; return true;
@@ -119,10 +122,9 @@ public class UpdateChecker implements Listener {
public void onJoin(PlayerJoinEvent e) { public void onJoin(PlayerJoinEvent e) {
if (e.getPlayer().hasPermission("placeholderapi.updatenotify")) { if (e.getPlayer().hasPermission("placeholderapi.updatenotify")) {
Msg.msg(e.getPlayer(), 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)" + "&e)"
, "&bis available at &ehttps://www.spigotmc.org/resources/placeholderapi." + RESOURCE_ID , "&bis available at &ehttps://modrinth.com/plugin/placeholderapi");
+ "/");
} }
} }
} }

View File

@@ -6,6 +6,8 @@
# Expansions: https://placeholderapi.com/ecloud # Expansions: https://placeholderapi.com/ecloud
# Wiki: https://wiki.placeholderapi.com/ # Wiki: https://wiki.placeholderapi.com/
# Discord: https://helpch.at/discord # 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. # No placeholders are provided with this plugin by default.
# Download placeholders: /papi ecloud # Download placeholders: /papi ecloud
check_updates: true check_updates: true
@@ -16,4 +18,5 @@ boolean:
'false': 'no' 'false': 'no'
date_format: MM/dd/yy HH:mm:ss date_format: MM/dd/yy HH:mm:ss
detect_malicious_expansions: true detect_malicious_expansions: true
use_adventure_provided_replacer: false
debug: false debug: false

View File

@@ -1,7 +1,26 @@
/*
* 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; package me.clip.placeholderapi;
import me.clip.placeholderapi.expansion.PlaceholderExpansion; import me.clip.placeholderapi.replacer.ComponentReplacer;
import me.clip.placeholderapi.expansion.Relational;
import me.clip.placeholderapi.replacer.ExactReplacer; import me.clip.placeholderapi.replacer.ExactReplacer;
import me.clip.placeholderapi.replacer.RelationalExactReplacer; import me.clip.placeholderapi.replacer.RelationalExactReplacer;
import me.clip.placeholderapi.replacer.Replacer; import me.clip.placeholderapi.replacer.Replacer;
@@ -30,9 +49,12 @@ public final class PAPIComponents {
*/ */
@NotNull @NotNull
public static Component setPlaceholders(final OfflinePlayer player, @NotNull final Component component) { 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 if (PlaceholderAPIPlugin.getInstance().getPlaceholderAPIConfig().useAdventureProvidedReplacer()) {
return component.replaceText(config -> config.match(PlaceholderAPI.PLACEHOLDER_PATTERN).replacement((result, builder) -> 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)))); builder.content(PERCENT_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
}
return ComponentReplacer.replace(component, str -> PlaceholderAPI.setPlaceholders(player, str));
} }
/** /**
@@ -84,8 +106,12 @@ public final class PAPIComponents {
*/ */
@NotNull @NotNull
public static Component setBracketPlaceholders(final OfflinePlayer player, @NotNull final Component component) { public static Component setBracketPlaceholders(final OfflinePlayer player, @NotNull final Component component) {
return component.replaceText(config -> config.match(PlaceholderAPI.BRACKET_PLACEHOLDER_PATTERN).replacement((result, builder) -> if (PlaceholderAPIPlugin.getInstance().getPlaceholderAPIConfig().useAdventureReplacer()) {
builder.content(BRACKET_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion)))); 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));
} }
/** /**
@@ -137,6 +163,7 @@ public final class PAPIComponents {
* @return The Component containing the parsed relational placeholders * @return The Component containing the parsed relational placeholders
*/ */
public static Component setRelationalPlaceholders(Player one, Player two, Component component) { 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) -> 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)))); builder.content(RELATIONAL_EXACT_REPLACER.apply(result.group(2), one, two, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
} }
@@ -154,9 +181,4 @@ public final class PAPIComponents {
return components.stream().map(line -> setRelationalPlaceholders(one, two, line)) return components.stream().map(line -> setRelationalPlaceholders(one, two, line))
.collect(Collectors.toList()); .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()
// }
} }

View File

@@ -0,0 +1,174 @@
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 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) {
return rebuild(component, replacer);
}
@NotNull
private static Component rebuild(@NotNull final Component component, @NotNull final Function<String, String> replacer) {
Component rebuilt;
if (component instanceof TextComponent) {
final TextComponent text = (TextComponent) component;
final String replaced = replacer.apply(text.content());
rebuilt = Component.text(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));
}
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));
if (!component.children().isEmpty()) {
final List<Component> children = new ArrayList<>();
for (Component child : component.children()) {
children.add(rebuild(child, replacer));
}
rebuilt = rebuilt.children(children);
}
return rebuilt;
}
@NotNull
private static Style rebuildStyle(@NotNull final Style style, @NotNull final Function<String, String> replacer) {
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));
}
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) {
final Object value = hover.value();
if (value instanceof Component) {
final Component rebuilt = rebuild((Component) value, replacer);
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);
}
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);
}
}

View File

@@ -1,3 +1,23 @@
/*
* 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; package me.clip.placeholderapi.replacer;
import me.clip.placeholderapi.expansion.PlaceholderExpansion; import me.clip.placeholderapi.expansion.PlaceholderExpansion;

View File

@@ -1,3 +1,23 @@
/*
* 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; package me.clip.placeholderapi.replacer;
import me.clip.placeholderapi.expansion.PlaceholderExpansion; import me.clip.placeholderapi.expansion.PlaceholderExpansion;