diff --git a/src/main/java/me/clip/placeholderapi/PlaceholderAPIPlugin.java b/src/main/java/me/clip/placeholderapi/PlaceholderAPIPlugin.java
index 004e122..dbd8e9f 100644
--- a/src/main/java/me/clip/placeholderapi/PlaceholderAPIPlugin.java
+++ b/src/main/java/me/clip/placeholderapi/PlaceholderAPIPlugin.java
@@ -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 {
diff --git a/src/main/java/me/clip/placeholderapi/ServerVersionResolver.java b/src/main/java/me/clip/placeholderapi/ServerVersionResolver.java
new file mode 100644
index 0000000..c0e8112
--- /dev/null
+++ b/src/main/java/me/clip/placeholderapi/ServerVersionResolver.java
@@ -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 .
+ */
+
+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);
+ }
+ }
+}
diff --git a/src/paper/java/me/clip/placeholderapi/PaperServerBuildInfoProvider.java b/src/paper/java/me/clip/placeholderapi/PaperServerBuildInfoProvider.java
new file mode 100644
index 0000000..1cf4149
--- /dev/null
+++ b/src/paper/java/me/clip/placeholderapi/PaperServerBuildInfoProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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 .
+ */
+
+package me.clip.placeholderapi;
+
+import io.papermc.paper.ServerBuildInfo;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+@ApiStatus.Internal
+public final class PaperServerBuildInfoProvider {
+
+ private PaperServerBuildInfoProvider() {
+ }
+
+ @NotNull
+ public static ServerVersionResolver.ServerBuild get() {
+ final ServerBuildInfo buildInfo = ServerBuildInfo.buildInfo();
+
+ return new ServerVersionResolver.ServerBuild(
+ buildInfo.minecraftVersionId(),
+ buildInfo.buildNumber()
+ );
+ }
+}
diff --git a/src/test/java/me/clip/placeholderapi/ServerVersionResolverTest.java b/src/test/java/me/clip/placeholderapi/ServerVersionResolverTest.java
new file mode 100644
index 0000000..cf1bcaf
--- /dev/null
+++ b/src/test/java/me/clip/placeholderapi/ServerVersionResolverTest.java
@@ -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 .
+ */
+
+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());
+ }
+}