aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0033-Expose-server-build-information.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0033-Expose-server-build-information.patch')
-rw-r--r--patches/server/0033-Expose-server-build-information.patch702
1 files changed, 702 insertions, 0 deletions
diff --git a/patches/server/0033-Expose-server-build-information.patch b/patches/server/0033-Expose-server-build-information.patch
new file mode 100644
index 0000000000..a6ae932964
--- /dev/null
+++ b/patches/server/0033-Expose-server-build-information.patch
@@ -0,0 +1,702 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Tue, 1 Mar 2016 14:32:43 -0600
+Subject: [PATCH] Expose server build information
+
+Co-authored-by: Zach Brown <[email protected]>
+Co-authored-by: Kyle Wood <[email protected]>
+Co-authored-by: Mark Vainomaa <[email protected]>
+Co-authored-by: Riley Park <[email protected]>
+Co-authored-by: Jake Potrebic <[email protected]>
+Co-authored-by: masmc05 <[email protected]>
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 404c2eefa88319e0eaf7b0d1d2696c91dd0e0e0b..e7c00486bd831578008c02fcda13f3a555e6a2f1 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -1,4 +1,5 @@
+ import io.papermc.paperweight.util.*
++import java.time.Instant
+
+ plugins {
+ java
+@@ -64,18 +65,24 @@ tasks.jar {
+
+ manifest {
+ val git = Git(rootProject.layout.projectDirectory.path)
++ val mcVersion = rootProject.providers.gradleProperty("mcVersion").get()
++ val build = System.getenv("BUILD_NUMBER") ?: null
+ val gitHash = git("rev-parse", "--short=7", "HEAD").getText().trim()
+- val implementationVersion = System.getenv("BUILD_NUMBER") ?: "\"$gitHash\""
++ val implementationVersion = "$mcVersion-${build ?: "DEV"}-$gitHash"
+ val date = git("show", "-s", "--format=%ci", gitHash).getText().trim() // Paper
+ val gitBranch = git("rev-parse", "--abbrev-ref", "HEAD").getText().trim() // Paper
+ attributes(
+ "Main-Class" to "org.bukkit.craftbukkit.Main",
+- "Implementation-Title" to "CraftBukkit",
+- "Implementation-Version" to "git-Paper-$implementationVersion",
++ "Implementation-Title" to "Paper",
++ "Implementation-Version" to implementationVersion,
+ "Implementation-Vendor" to date, // Paper
+- "Specification-Title" to "Bukkit",
++ "Specification-Title" to "Paper",
+ "Specification-Version" to project.version,
+- "Specification-Vendor" to "Bukkit Team",
++ "Specification-Vendor" to "Paper Team",
++ "Brand-Id" to "papermc:paper",
++ "Brand-Name" to "Paper",
++ "Build-Number" to (build ?: ""),
++ "Build-Time" to Instant.now().toString(),
+ "Git-Branch" to gitBranch, // Paper
+ "Git-Commit" to gitHash, // Paper
+ "CraftBukkit-Package-Version" to paperweight.craftBukkitPackageVersion.get(), // Paper
+diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..532306cacd52579cdf37e4aca25887b1ed3ba6a1
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
+@@ -0,0 +1,146 @@
++package com.destroystokyo.paper;
++
++import com.destroystokyo.paper.util.VersionFetcher;
++import com.google.common.base.Charsets;
++import com.google.common.io.Resources;
++import com.google.gson.Gson;
++import com.google.gson.JsonArray;
++import com.google.gson.JsonElement;
++import com.google.gson.JsonObject;
++import com.google.gson.JsonSyntaxException;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.ServerBuildInfo;
++import java.io.BufferedReader;
++import java.io.IOException;
++import java.io.InputStreamReader;
++import java.net.HttpURLConnection;
++import java.net.URI;
++import java.util.Optional;
++import java.util.OptionalInt;
++import java.util.stream.StreamSupport;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.event.ClickEvent;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.format.TextDecoration;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.slf4j.Logger;
++
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.format.TextColor.color;
++
++@DefaultQualifier(NonNull.class)
++public class PaperVersionFetcher implements VersionFetcher {
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static final int DISTANCE_ERROR = -1;
++ private static final int DISTANCE_UNKNOWN = -2;
++ private static final String DOWNLOAD_PAGE = "https://papermc.io/downloads/paper";
++
++ @Override
++ public long getCacheTime() {
++ return 720000;
++ }
++
++ @Override
++ public Component getVersionMessage(final String serverVersion) {
++ final Component updateMessage;
++ final ServerBuildInfo build = ServerBuildInfo.buildInfo();
++ if (build.buildNumber().isEmpty() && build.gitCommit().isEmpty()) {
++ updateMessage = text("You are running a development version without access to version information", color(0xFF5300));
++ } else {
++ updateMessage = getUpdateStatusMessage("PaperMC/Paper", build);
++ }
++ final @Nullable Component history = this.getHistory();
++
++ return history != null ? Component.textOfChildren(updateMessage, Component.newline(), history) : updateMessage;
++ }
++
++ private static Component getUpdateStatusMessage(final String repo, final ServerBuildInfo build) {
++ int distance = DISTANCE_ERROR;
++
++ final OptionalInt buildNumber = build.buildNumber();
++ if (buildNumber.isPresent()) {
++ distance = fetchDistanceFromSiteApi(build, buildNumber.getAsInt());
++ } else {
++ final Optional<String> gitBranch = build.gitBranch();
++ final Optional<String> gitCommit = build.gitCommit();
++ if (gitBranch.isPresent() && gitCommit.isPresent()) {
++ distance = fetchDistanceFromGitHub(repo, gitBranch.get(), gitCommit.get());
++ }
++ }
++
++ return switch (distance) {
++ case DISTANCE_ERROR -> text("Error obtaining version information", NamedTextColor.YELLOW);
++ case 0 -> text("You are running the latest version", NamedTextColor.GREEN);
++ case DISTANCE_UNKNOWN -> text("Unknown version", NamedTextColor.YELLOW);
++ default -> text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW)
++ .append(Component.newline())
++ .append(text("Download the new version at: ")
++ .append(text(DOWNLOAD_PAGE, NamedTextColor.GOLD)
++ .hoverEvent(text("Click to open", NamedTextColor.WHITE))
++ .clickEvent(ClickEvent.openUrl(DOWNLOAD_PAGE))));
++ };
++ }
++
++ private static int fetchDistanceFromSiteApi(final ServerBuildInfo build, final int jenkinsBuild) {
++ try {
++ try (final BufferedReader reader = Resources.asCharSource(
++ URI.create("https://api.papermc.io/v2/projects/paper/versions/" + build.minecraftVersionId()).toURL(),
++ Charsets.UTF_8
++ ).openBufferedStream()) {
++ final JsonObject json = new Gson().fromJson(reader, JsonObject.class);
++ final JsonArray builds = json.getAsJsonArray("builds");
++ final int latest = StreamSupport.stream(builds.spliterator(), false)
++ .mapToInt(JsonElement::getAsInt)
++ .max()
++ .orElseThrow();
++ return latest - jenkinsBuild;
++ } catch (final JsonSyntaxException ex) {
++ LOGGER.error("Error parsing json from Paper's downloads API", ex);
++ return DISTANCE_ERROR;
++ }
++ } catch (final IOException e) {
++ LOGGER.error("Error while parsing version", e);
++ return DISTANCE_ERROR;
++ }
++ }
++
++ // Contributed by Techcable <[email protected]> in GH-65
++ private static int fetchDistanceFromGitHub(final String repo, final String branch, final String hash) {
++ try {
++ final HttpURLConnection connection = (HttpURLConnection) URI.create("https://api.github.com/repos/%s/compare/%s...%s".formatted(repo, branch, hash)).toURL().openConnection();
++ connection.connect();
++ if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) return DISTANCE_UNKNOWN; // Unknown commit
++ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) {
++ final JsonObject obj = new Gson().fromJson(reader, JsonObject.class);
++ final String status = obj.get("status").getAsString();
++ return switch (status) {
++ case "identical" -> 0;
++ case "behind" -> obj.get("behind_by").getAsInt();
++ default -> DISTANCE_ERROR;
++ };
++ } catch (final JsonSyntaxException | NumberFormatException e) {
++ LOGGER.error("Error parsing json from GitHub's API", e);
++ return DISTANCE_ERROR;
++ }
++ } catch (final IOException e) {
++ LOGGER.error("Error while parsing version", e);
++ return DISTANCE_ERROR;
++ }
++ }
++
++ private @Nullable Component getHistory() {
++ final VersionHistoryManager.@Nullable VersionData data = VersionHistoryManager.INSTANCE.getVersionData();
++ if (data == null) {
++ return null;
++ }
++
++ final @Nullable String oldVersion = data.getOldVersion();
++ if (oldVersion == null) {
++ return null;
++ }
++
++ return text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC);
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java b/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..660b2ec6b63a4ceffee44ab11f54dfa7c0d0996f
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java
+@@ -0,0 +1,153 @@
++package com.destroystokyo.paper;
++
++import com.google.common.base.MoreObjects;
++import com.google.gson.Gson;
++import com.google.gson.JsonSyntaxException;
++import java.io.BufferedReader;
++import java.io.BufferedWriter;
++import java.io.IOException;
++import java.nio.charset.StandardCharsets;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.nio.file.Paths;
++import java.nio.file.StandardOpenOption;
++import java.util.Objects;
++import java.util.logging.Level;
++import java.util.logging.Logger;
++import org.bukkit.Bukkit;
++
++import javax.annotation.Nonnull;
++import javax.annotation.Nullable;
++
++public enum VersionHistoryManager {
++ INSTANCE;
++
++ private final Gson gson = new Gson();
++
++ private final Logger logger = Bukkit.getLogger();
++
++ private VersionData currentData = null;
++
++ VersionHistoryManager() {
++ final Path path = Paths.get("version_history.json");
++
++ if (Files.exists(path)) {
++ // Basic file santiy checks
++ if (!Files.isRegularFile(path)) {
++ if (Files.isDirectory(path)) {
++ logger.severe(path + " is a directory, cannot be used for version history");
++ } else {
++ logger.severe(path + " is not a regular file, cannot be used for version history");
++ }
++ // We can't continue
++ return;
++ }
++
++ try (final BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
++ currentData = gson.fromJson(reader, VersionData.class);
++ } catch (final IOException e) {
++ logger.log(Level.SEVERE, "Failed to read version history file '" + path + "'", e);
++ return;
++ } catch (final JsonSyntaxException e) {
++ logger.log(Level.SEVERE, "Invalid json syntax for file '" + path + "'", e);
++ return;
++ }
++
++ final String version = Bukkit.getVersion();
++ if (version == null) {
++ logger.severe("Failed to retrieve current version");
++ return;
++ }
++
++ if (currentData == null) {
++ // Empty file
++ currentData = new VersionData();
++ currentData.setCurrentVersion(version);
++ writeFile(path);
++ return;
++ }
++
++ if (!version.equals(currentData.getCurrentVersion())) {
++ // The version appears to have changed
++ currentData.setOldVersion(currentData.getCurrentVersion());
++ currentData.setCurrentVersion(version);
++ writeFile(path);
++ }
++ } else {
++ // File doesn't exist, start fresh
++ currentData = new VersionData();
++ // oldVersion is null
++ currentData.setCurrentVersion(Bukkit.getVersion());
++ writeFile(path);
++ }
++ }
++
++ private void writeFile(@Nonnull final Path path) {
++ try (final BufferedWriter writer = Files.newBufferedWriter(
++ path,
++ StandardCharsets.UTF_8,
++ StandardOpenOption.WRITE,
++ StandardOpenOption.CREATE,
++ StandardOpenOption.TRUNCATE_EXISTING
++ )) {
++ gson.toJson(currentData, writer);
++ } catch (final IOException e) {
++ logger.log(Level.SEVERE, "Failed to write to version history file", e);
++ }
++ }
++
++ @Nullable
++ public VersionData getVersionData() {
++ return currentData;
++ }
++
++ public static class VersionData {
++ private String oldVersion;
++
++ private String currentVersion;
++
++ @Nullable
++ public String getOldVersion() {
++ return oldVersion;
++ }
++
++ public void setOldVersion(@Nullable String oldVersion) {
++ this.oldVersion = oldVersion;
++ }
++
++ @Nullable
++ public String getCurrentVersion() {
++ return currentVersion;
++ }
++
++ public void setCurrentVersion(@Nullable String currentVersion) {
++ this.currentVersion = currentVersion;
++ }
++
++ @Override
++ public String toString() {
++ return MoreObjects.toStringHelper(this)
++ .add("oldVersion", oldVersion)
++ .add("currentVersion", currentVersion)
++ .toString();
++ }
++
++ @Override
++ public boolean equals(@Nullable Object o) {
++ if (this == o) {
++ return true;
++ }
++ if (o == null || getClass() != o.getClass()) {
++ return false;
++ }
++ final VersionData versionData = (VersionData) o;
++ return Objects.equals(oldVersion, versionData.oldVersion) &&
++ Objects.equals(currentVersion, versionData.currentVersion);
++ }
++
++ @Override
++ public int hashCode() {
++ return Objects.hash(oldVersion, currentVersion);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..790bad0494454ca12ee152e3de6da3da634d9b20
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java
+@@ -0,0 +1,104 @@
++package io.papermc.paper;
++
++import com.google.common.base.Strings;
++import io.papermc.paper.util.JarManifests;
++import java.time.Instant;
++import java.time.temporal.ChronoUnit;
++import java.util.Optional;
++import java.util.OptionalInt;
++import java.util.jar.Manifest;
++import net.kyori.adventure.key.Key;
++import net.minecraft.SharedConstants;
++import org.bukkit.craftbukkit.CraftServer;
++import org.bukkit.craftbukkit.Main;
++import org.jetbrains.annotations.NotNull;
++
++public record ServerBuildInfoImpl(
++ Key brandId,
++ String brandName,
++ String minecraftVersionId,
++ String minecraftVersionName,
++ OptionalInt buildNumber,
++ Instant buildTime,
++ Optional<String> gitBranch,
++ Optional<String> gitCommit
++) implements ServerBuildInfo {
++ private static final String ATTRIBUTE_BRAND_ID = "Brand-Id";
++ private static final String ATTRIBUTE_BRAND_NAME = "Brand-Name";
++ private static final String ATTRIBUTE_BUILD_TIME = "Build-Time";
++ private static final String ATTRIBUTE_BUILD_NUMBER = "Build-Number";
++ private static final String ATTRIBUTE_GIT_BRANCH = "Git-Branch";
++ private static final String ATTRIBUTE_GIT_COMMIT = "Git-Commit";
++
++ private static final String BRAND_PAPER_NAME = "Paper";
++
++ private static final String BUILD_DEV = "DEV";
++
++ public ServerBuildInfoImpl() {
++ this(JarManifests.manifest(CraftServer.class));
++ }
++
++ private ServerBuildInfoImpl(final Manifest manifest) {
++ this(
++ getManifestAttribute(manifest, ATTRIBUTE_BRAND_ID)
++ .map(Key::key)
++ .orElse(BRAND_PAPER_ID),
++ getManifestAttribute(manifest, ATTRIBUTE_BRAND_NAME)
++ .orElse(BRAND_PAPER_NAME),
++ SharedConstants.getCurrentVersion().getId(),
++ SharedConstants.getCurrentVersion().getName(),
++ getManifestAttribute(manifest, ATTRIBUTE_BUILD_NUMBER)
++ .map(Integer::parseInt)
++ .map(OptionalInt::of)
++ .orElse(OptionalInt.empty()),
++ getManifestAttribute(manifest, ATTRIBUTE_BUILD_TIME)
++ .map(Instant::parse)
++ .orElse(Main.BOOT_TIME),
++ getManifestAttribute(manifest, ATTRIBUTE_GIT_BRANCH),
++ getManifestAttribute(manifest, ATTRIBUTE_GIT_COMMIT)
++ );
++ }
++
++ @Override
++ public boolean isBrandCompatible(final @NotNull Key brandId) {
++ return brandId.equals(this.brandId);
++ }
++
++ @Override
++ public @NotNull String asString(final @NotNull StringRepresentation representation) {
++ final StringBuilder sb = new StringBuilder();
++ sb.append(this.minecraftVersionId);
++ sb.append('-');
++ if (this.buildNumber.isPresent()) {
++ sb.append(this.buildNumber.getAsInt());
++ } else {
++ sb.append(BUILD_DEV);
++ }
++ final boolean hasGitBranch = this.gitBranch.isPresent();
++ final boolean hasGitCommit = this.gitCommit.isPresent();
++ if (hasGitBranch || hasGitCommit) {
++ sb.append('-');
++ }
++ if (hasGitBranch && representation == StringRepresentation.VERSION_FULL) {
++ sb.append(this.gitBranch.get());
++ if (hasGitCommit) {
++ sb.append('@');
++ }
++ }
++ if (hasGitCommit) {
++ sb.append(this.gitCommit.get());
++ }
++ if (representation == StringRepresentation.VERSION_FULL) {
++ sb.append(' ');
++ sb.append('(');
++ sb.append(this.buildTime.truncatedTo(ChronoUnit.SECONDS));
++ sb.append(')');
++ }
++ return sb.toString();
++ }
++
++ private static Optional<String> getManifestAttribute(final Manifest manifest, final String name) {
++ final String value = manifest != null ? manifest.getMainAttributes().getValue(name) : null;
++ return Optional.ofNullable(Strings.emptyToNull(value));
++ }
++}
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index e9d56d75b7c648f04d3a56942b2866090c570129..57ec168bac3727feb734e28cebc680328c1c4aec 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -43,7 +43,6 @@ import java.util.Set;
+ import java.util.UUID;
+ import java.util.concurrent.CompletableFuture;
+ import java.util.concurrent.Executor;
+-import java.util.concurrent.RejectedExecutionException;
+ import java.util.concurrent.atomic.AtomicReference;
+ import java.util.function.BooleanSupplier;
+ import java.util.function.Consumer;
+@@ -191,8 +190,6 @@ import net.minecraft.world.phys.Vec2;
+ import net.minecraft.world.phys.Vec3;
+ import org.bukkit.Bukkit;
+ import org.bukkit.craftbukkit.CraftRegistry;
+-import org.bukkit.craftbukkit.CraftServer;
+-import org.bukkit.craftbukkit.Main;
+ import org.bukkit.event.server.ServerLoadEvent;
+ // CraftBukkit end
+
+@@ -1702,7 +1699,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ @DontObfuscate
+ public String getServerModName() {
+- return "Spigot"; // Spigot - Spigot > // CraftBukkit - cb > vanilla!
++ return io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); // Paper
+ }
+
+ public SystemReport fillSystemReport(SystemReport details) {
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index b51c3f8c485496734ea58c15377a1215a334c765..48107f8eb50483430053b990496862d71c9f8a3e 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -218,6 +218,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ // Paper end - initialize global and world-defaults configuration
+ io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command
+ com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics
++ com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now
+
+ this.setPvpAllowed(dedicatedserverproperties.pvp);
+ this.setFlightAllowed(dedicatedserverproperties.allowFlight);
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java
+index f077b8ff0bf0d96628db3569132696b68fd79921..5f11f5b16766f9d1d5640ae037e259bed9020384 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java
+@@ -18,8 +18,10 @@ public class CraftCrashReport implements Supplier<String> {
+
+ @Override
+ public String get() {
++ final io.papermc.paper.ServerBuildInfo build = io.papermc.paper.ServerBuildInfo.buildInfo(); // Paper
+ StringWriter value = new StringWriter();
+ try {
++ value.append("\n BrandInfo: ").append(String.format("%s (%s) version %s", build.brandName(), build.brandId(), build.asString(io.papermc.paper.ServerBuildInfo.StringRepresentation.VERSION_FULL))); // Paper
+ value.append("\n Running: ").append(Bukkit.getName()).append(" version ").append(Bukkit.getVersion()).append(" (Implementing API version ").append(Bukkit.getBukkitVersion()).append(") ").append(String.valueOf(MinecraftServer.getServer().usesAuthentication()));
+ value.append("\n Plugins: {");
+ for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index a2f784b28c0d974ee45d61d6a3a0096dd7161d3e..7c97ec4aa57562a8383a40e493eaa8a3697208bb 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -11,8 +11,6 @@ import com.google.common.collect.MapMaker;
+ import com.mojang.authlib.GameProfile;
+ import com.mojang.brigadier.StringReader;
+ import com.mojang.brigadier.exceptions.CommandSyntaxException;
+-import com.mojang.brigadier.tree.CommandNode;
+-import com.mojang.brigadier.tree.LiteralCommandNode;
+ import com.mojang.serialization.Dynamic;
+ import com.mojang.serialization.Lifecycle;
+ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+@@ -27,7 +25,6 @@ import java.net.InetAddress;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.Date;
+-import java.util.HashMap;
+ import java.util.HashSet;
+ import java.util.Iterator;
+ import java.util.LinkedHashMap;
+@@ -155,7 +152,6 @@ import org.bukkit.craftbukkit.ban.CraftProfileBanList;
+ import org.bukkit.craftbukkit.block.data.CraftBlockData;
+ import org.bukkit.craftbukkit.boss.CraftBossBar;
+ import org.bukkit.craftbukkit.boss.CraftKeyedBossbar;
+-import org.bukkit.craftbukkit.command.BukkitCommandWrapper;
+ import org.bukkit.craftbukkit.command.CraftCommandMap;
+ import org.bukkit.craftbukkit.command.VanillaCommandWrapper;
+ import org.bukkit.craftbukkit.entity.CraftEntityFactory;
+@@ -251,7 +247,6 @@ import org.bukkit.plugin.PluginManager;
+ import org.bukkit.plugin.ServicesManager;
+ import org.bukkit.plugin.SimplePluginManager;
+ import org.bukkit.plugin.SimpleServicesManager;
+-import org.bukkit.plugin.java.JavaPluginLoader;
+ import org.bukkit.plugin.messaging.Messenger;
+ import org.bukkit.plugin.messaging.StandardMessenger;
+ import org.bukkit.profile.PlayerProfile;
+@@ -268,7 +263,7 @@ import org.yaml.snakeyaml.error.MarkedYAMLException;
+ import net.md_5.bungee.api.chat.BaseComponent; // Spigot
+
+ public final class CraftServer implements Server {
+- private final String serverName = "CraftBukkit";
++ private final String serverName = io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); // Paper
+ private final String serverVersion;
+ private final String bukkitVersion = Versioning.getBukkitVersion();
+ private final Logger logger = Logger.getLogger("Minecraft");
+@@ -324,7 +319,7 @@ public final class CraftServer implements Server {
+ return player.getBukkitEntity();
+ }
+ }));
+- this.serverVersion = CraftServer.class.getPackage().getImplementationVersion();
++ this.serverVersion = io.papermc.paper.ServerBuildInfo.buildInfo().asString(io.papermc.paper.ServerBuildInfo.StringRepresentation.VERSION_SIMPLE); // Paper - improve version
+ this.structureManager = new CraftStructureManager(console.getStructureManager(), console.registryAccess());
+ this.dataPackManager = new CraftDataPackManager(this.getServer().getPackRepository());
+ this.serverTickManager = new CraftServerTickManager(console.tickRateManager());
+@@ -598,6 +593,13 @@ public final class CraftServer implements Server {
+ return this.bukkitVersion;
+ }
+
++ // Paper start - expose game version
++ @Override
++ public String getMinecraftVersion() {
++ return console.getServerVersion();
++ }
++ // Paper end
++
+ @Override
+ public List<CraftPlayer> getOnlinePlayers() {
+ return this.playerView;
+diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
+index bd21da04390e486731a260f8fb0c70921320198e..14f63c179428bee61d3b931ea309f4c94b89a6cc 100644
+--- a/src/main/java/org/bukkit/craftbukkit/Main.java
++++ b/src/main/java/org/bukkit/craftbukkit/Main.java
+@@ -15,6 +15,7 @@ import joptsimple.OptionSet;
+ import joptsimple.util.PathConverter;
+
+ public class Main {
++ public static final java.time.Instant BOOT_TIME = java.time.Instant.now(); // Paper - track initial start time
+ public static boolean useJline = true;
+ public static boolean useConsole = true;
+
+@@ -252,13 +253,26 @@ public class Main {
+ deadline.add(Calendar.DAY_OF_YEAR, -7);
+ if (buildDate.before(deadline.getTime())) {
+ System.err.println("*** Error, this build is outdated ***");
+- System.err.println("*** Please download a new build as per instructions from https://www.spigotmc.org/go/outdated-spigot ***");
++ System.err.println("*** Please download a new build as per instructions from https://papermc.io/downloads/paper ***"); // Paper
+ System.err.println("*** Server will start in 20 seconds ***");
+ Thread.sleep(TimeUnit.SECONDS.toMillis(20));
+ }
+ }
+
+ System.setProperty("library.jansi.version", "Paper"); // Paper - set meaningless jansi version to prevent git builds from crashing on Windows
++ // Paper start - Log Java and OS versioning to help with debugging plugin issues
++ java.lang.management.RuntimeMXBean runtimeMX = java.lang.management.ManagementFactory.getRuntimeMXBean();
++ java.lang.management.OperatingSystemMXBean osMX = java.lang.management.ManagementFactory.getOperatingSystemMXBean();
++ if (runtimeMX != null && osMX != null) {
++ String javaInfo = "Java " + runtimeMX.getSpecVersion() + " (" + runtimeMX.getVmName() + " " + runtimeMX.getVmVersion() + ")";
++ String osInfo = "Host: " + osMX.getName() + " " + osMX.getVersion() + " (" + osMX.getArch() + ")";
++
++ System.out.println("System Info: " + javaInfo + " " + osInfo);
++ } else {
++ System.out.println("Unable to read system info");
++ }
++ // Paper end - Log Java and OS versioning to help with debugging plugin issues
++
+ System.out.println("Loading libraries, please wait...");
+ net.minecraft.server.Main.main(options);
+ } catch (Throwable t) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index a356d4d19e4b7a3d08c80a137609d1ed9db7c1b1..e85faddc157869824e30426a7ca2c577552aea9c 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -464,6 +464,11 @@ public final class CraftMagicNumbers implements UnsafeValues {
+ public String getTimingsServerName() {
+ return io.papermc.paper.configuration.GlobalConfiguration.get().timings.serverName;
+ }
++
++ @Override
++ public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() {
++ return new com.destroystokyo.paper.PaperVersionFetcher();
++ }
+ // Paper end
+
+ @Override
+diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
+index f697d45e0ac4e9cdc8a46121510a04c0f294d91f..e086765dec32241bc5a77afcf072c77a40c6d785 100644
+--- a/src/main/java/org/spigotmc/WatchdogThread.java
++++ b/src/main/java/org/spigotmc/WatchdogThread.java
+@@ -19,7 +19,7 @@ public class WatchdogThread extends Thread
+
+ private WatchdogThread(long timeoutTime, boolean restart)
+ {
+- super( "Spigot Watchdog Thread" );
++ super( "Paper Watchdog Thread" );
+ this.timeoutTime = timeoutTime;
+ this.restart = restart;
+ }
+@@ -65,14 +65,14 @@ public class WatchdogThread extends Thread
+ {
+ Logger log = Bukkit.getServer().getLogger();
+ log.log( Level.SEVERE, "------------------------------" );
+- log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Spigot bug." );
++ log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Paper bug." ); // Paper
+ log.log( Level.SEVERE, "If you see a plugin in the Server thread dump below, then please report it to that author" );
+ log.log( Level.SEVERE, "\t *Especially* if it looks like HTTP or MySQL operations are occurring" );
+ log.log( Level.SEVERE, "If you see a world save or edit, then it means you did far more than your server can handle at once" );
+ log.log( Level.SEVERE, "\t If this is the case, consider increasing timeout-time in spigot.yml but note that this will replace the crash with LARGE lag spikes" );
+- log.log( Level.SEVERE, "If you are unsure or still think this is a Spigot bug, please report to https://www.spigotmc.org/" );
++ log.log( Level.SEVERE, "If you are unsure or still think this is a Paper bug, please report this to https://github.com/PaperMC/Paper/issues" );
+ log.log( Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports" );
+- log.log( Level.SEVERE, "Spigot version: " + Bukkit.getServer().getVersion() );
++ log.log( Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion() );
+ //
+ if ( net.minecraft.world.level.Level.lastPhysicsProblem != null )
+ {
+@@ -82,7 +82,7 @@ public class WatchdogThread extends Thread
+ }
+ //
+ log.log( Level.SEVERE, "------------------------------" );
+- log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Spigot!):" );
++ log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
+ WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
+ log.log( Level.SEVERE, "------------------------------" );
+ //
+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo b/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo
+new file mode 100644
+index 0000000000000000000000000000000000000000..79b4b25784cfeabd5f619ed5454ef843f35041db
+--- /dev/null
++++ b/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo
+@@ -0,0 +1 @@
++io.papermc.paper.ServerBuildInfoImpl