From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Zach Brown <zach@zachbr.io>
Date: Mon, 27 May 2019 01:10:06 -0500
Subject: [PATCH] Expose server build information

Co-authored-by: Professor Bloodstone <git@bloodstone.dev>
Co-authored-by: Mark Vainomaa <mikroskeem@mikroskeem.eu>
Co-authored-by: masmc05 <masmc05@gmail.com>
Co-authored-by: Riley Park <rileysebastianpark@gmail.com>

diff --git a/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java b/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..023cc52a9e28e1238c7452c0f3f577f2850fd861
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java
@@ -0,0 +1,47 @@
+package com.destroystokyo.paper.util;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public interface VersionFetcher {
+
+    /**
+     * Amount of time to cache results for in milliseconds
+     * <p>
+     * Negative values will never cache.
+     *
+     * @return cache time
+     */
+    long getCacheTime();
+
+    /**
+     * Gets the version message to cache and show to command senders.
+     *
+     * <p>NOTE: This is run in a new thread separate from that of the command processing thread</p>
+     *
+     * @param serverVersion the current version of the server (will match {@link Bukkit#getVersion()})
+     * @return the message to show when requesting a version
+     */
+    Component getVersionMessage(String serverVersion);
+
+    @ApiStatus.Internal
+    class DummyVersionFetcher implements VersionFetcher {
+
+        @Override
+        public long getCacheTime() {
+            return -1;
+        }
+
+        @Override
+        public Component getVersionMessage(final String serverVersion) {
+            Bukkit.getLogger().warning("Version provider has not been set, cannot check for updates!");
+            Bukkit.getLogger().info("Override the default implementation of org.bukkit.UnsafeValues#getVersionFetcher()");
+            new Throwable().printStackTrace();
+            return Component.text("Unable to check for updates. No version provider set.", NamedTextColor.RED);
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/ServerBuildInfo.java b/src/main/java/io/papermc/paper/ServerBuildInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..652ff54e7c50412503725d628bfe72ed03059790
--- /dev/null
+++ b/src/main/java/io/papermc/paper/ServerBuildInfo.java
@@ -0,0 +1,122 @@
+package io.papermc.paper;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.OptionalInt;
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.util.Services;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Information about the current server build.
+ */
+@NullMarked
+@ApiStatus.NonExtendable
+public interface ServerBuildInfo {
+    /**
+     * The brand id for Paper.
+     */
+    Key BRAND_PAPER_ID = Key.key("papermc", "paper");
+
+    /**
+     * Gets the {@code ServerBuildInfo}.
+     *
+     * @return the {@code ServerBuildInfo}
+     */
+    static ServerBuildInfo buildInfo() {
+        //<editor-fold defaultstate="collapsed" desc="Holder">
+        final class Holder {
+            static final Optional<ServerBuildInfo> INSTANCE = Services.service(ServerBuildInfo.class);
+        }
+        //</editor-fold>
+        return Holder.INSTANCE.orElseThrow();
+    }
+
+    /**
+     * Gets the brand id of the server.
+     *
+     * @return the brand id of the server (e.g. "papermc:paper")
+     */
+    Key brandId();
+
+    /**
+     * Checks if the current server supports the specified brand.
+     *
+     * @param brandId the brand to check (e.g. "papermc:folia")
+     * @return {@code true} if the server supports the specified brand
+     */
+    @ApiStatus.Experimental
+    boolean isBrandCompatible(final Key brandId);
+
+    /**
+     * Gets the brand name of the server.
+     *
+     * @return the brand name of the server (e.g. "Paper")
+     */
+    String brandName();
+
+    /**
+     * Gets the Minecraft version id.
+     *
+     * @return the Minecraft version id (e.g. "1.20.4", "1.20.2-pre2", "23w31a")
+     */
+    String minecraftVersionId();
+
+    /**
+     * Gets the Minecraft version name.
+     *
+     * @return the Minecraft version name (e.g. "1.20.4", "1.20.2 Pre-release 2", "23w31a")
+     */
+    String minecraftVersionName();
+
+    /**
+     * Gets the build number.
+     *
+     * @return the build number
+     */
+    OptionalInt buildNumber();
+
+    /**
+     * Gets the build time.
+     *
+     * @return the build time
+     */
+    Instant buildTime();
+
+    /**
+     * Gets the git commit branch.
+     *
+     * @return the git commit branch
+     */
+    Optional<String> gitBranch();
+
+    /**
+     * Gets the git commit hash.
+     *
+     * @return the git commit hash
+     */
+    Optional<String> gitCommit();
+
+    /**
+     * Creates a string representation of the server build information.
+     *
+     * @param representation the type of representation
+     * @return a string
+     */
+    String asString(final StringRepresentation representation);
+
+    /**
+     * String representation types.
+     */
+    enum StringRepresentation {
+        /**
+         * A simple version string, in format {@code <minecraftVersionId>-<buildNumber>-<gitCommit>}.
+         */
+        VERSION_SIMPLE,
+        /**
+         * A simple version string, in format {@code <minecraftVersionId>-<buildNumber>-<gitBranch>@<gitCommit> (<buildTime>)}.
+         */
+        VERSION_FULL,
+    }
+}
diff --git a/src/main/java/io/papermc/paper/util/JarManifests.java b/src/main/java/io/papermc/paper/util/JarManifests.java
new file mode 100644
index 0000000000000000000000000000000000000000..7915a70d676b1205dcae39259f670af258a1ab9b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/JarManifests.java
@@ -0,0 +1,38 @@
+package io.papermc.paper.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.jar.Manifest;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+@NullMarked
+@ApiStatus.Internal
+public final class JarManifests {
+    private JarManifests() {
+    }
+
+    private static final Map<ClassLoader, Manifest> MANIFESTS = Collections.synchronizedMap(new WeakHashMap<>());
+
+    public static @Nullable Manifest manifest(final Class<?> clazz) {
+        return MANIFESTS.computeIfAbsent(clazz.getClassLoader(), classLoader -> {
+            final String classLocation = "/" + clazz.getName().replace(".", "/") + ".class";
+            final URL resource = clazz.getResource(classLocation);
+            if (resource == null) {
+                return null;
+            }
+            final String classFilePath = resource.toString().replace("\\", "/");
+            final String archivePath = classFilePath.substring(0, classFilePath.length() - classLocation.length());
+            try (final InputStream stream = new URL(archivePath + "/META-INF/MANIFEST.MF").openStream()) {
+                return new Manifest(stream);
+            } catch (final IOException ex) {
+                return null;
+            }
+        });
+    }
+}
diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java
index e4f7ff41d7205994fef87989a7955d7b8fe4d7f4..75e0c5b884363be03876103e0d66e67de03c4856 100644
--- a/src/main/java/org/bukkit/Bukkit.java
+++ b/src/main/java/org/bukkit/Bukkit.java
@@ -110,13 +110,26 @@ public final class Bukkit {
         }
 
         Bukkit.server = server;
-        server.getLogger().info("This server is running " + getName() + " version " + getVersion() + " (Implementing API version " + getBukkitVersion() + ")");
+        // Paper start - add git information
+        server.getLogger().info(getVersionMessage());
+    }
+    /**
+      * Gets message describing the version server is running.
+      *
+      * @return message describing the version server is running
+      */
+    @NotNull
+    public static String getVersionMessage() {
+        final io.papermc.paper.ServerBuildInfo version = io.papermc.paper.ServerBuildInfo.buildInfo();
+        return "This server is running " + getName() + " version " + version.asString(io.papermc.paper.ServerBuildInfo.StringRepresentation.VERSION_FULL) + " (Implementing API version " + getBukkitVersion() + ")";
+        // Paper end
     }
 
     /**
      * Gets the name of this server implementation.
      *
      * @return name of this server implementation
+     * @see io.papermc.paper.ServerBuildInfo#brandName()
      */
     @NotNull
     public static String getName() {
@@ -127,6 +140,7 @@ public final class Bukkit {
      * Gets the version string of this server implementation.
      *
      * @return version of this server implementation
+     * @see io.papermc.paper.ServerBuildInfo
      */
     @NotNull
     public static String getVersion() {
@@ -143,6 +157,20 @@ public final class Bukkit {
         return server.getBukkitVersion();
     }
 
+    // Paper start - expose game version
+    /**
+     * Gets the version of game this server implements
+     *
+     * @return version of game
+     * @see io.papermc.paper.ServerBuildInfo#minecraftVersionId()
+     * @see io.papermc.paper.ServerBuildInfo#minecraftVersionName()
+     */
+    @NotNull
+    public static String getMinecraftVersion() {
+        return server.getMinecraftVersion();
+    }
+    // Paper end
+
     /**
      * Gets a view of all currently logged in players. This {@linkplain
      * Collections#unmodifiableCollection(Collection) view} is a reused
diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java
index 4f15cc4bcc07d3061dd94b20fc77f549ddfcbb6b..2ed640d5a0027f7a94a5cf4555741c27c9b1b3a4 100644
--- a/src/main/java/org/bukkit/Server.java
+++ b/src/main/java/org/bukkit/Server.java
@@ -120,6 +120,16 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
     @NotNull
     public String getBukkitVersion();
 
+    // Paper start - expose game version
+    /**
+     * Gets the version of game this server implements
+     *
+     * @return version of game
+     */
+    @NotNull
+    String getMinecraftVersion();
+    // Paper end
+
     /**
      * Gets a view of all currently logged in players. This {@linkplain
      * Collections#unmodifiableCollection(Collection) view} is a reused
diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java
index aec092e019667d53faf3e7352799772804d5d260..012b46c82d9d06d1d2da8da626fc5cde6e9e2ca4 100644
--- a/src/main/java/org/bukkit/UnsafeValues.java
+++ b/src/main/java/org/bukkit/UnsafeValues.java
@@ -156,4 +156,13 @@ public interface UnsafeValues {
         return !Bukkit.getUnsafe().isSupportedApiVersion(plugin.getDescription().getAPIVersion());
     }
     // Paper end
+
+    // Paper start
+    /**
+     * Called once by the version command on first use, then cached.
+     */
+    default com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() {
+        return new com.destroystokyo.paper.util.VersionFetcher.DummyVersionFetcher();
+    }
+    // Paper end
 }
diff --git a/src/main/java/org/bukkit/command/defaults/VersionCommand.java b/src/main/java/org/bukkit/command/defaults/VersionCommand.java
index 263208d3cba36cb80c9ee4e3022ef702ea113df2..e64bb57f74e6d6f78927be228825b3e0bdf41f48 100644
--- a/src/main/java/org/bukkit/command/defaults/VersionCommand.java
+++ b/src/main/java/org/bukkit/command/defaults/VersionCommand.java
@@ -25,8 +25,25 @@ import org.bukkit.plugin.Plugin;
 import org.bukkit.plugin.PluginDescriptionFile;
 import org.bukkit.util.StringUtil;
 import org.jetbrains.annotations.NotNull;
+// Paper start - version command 2.0
+import com.destroystokyo.paper.util.VersionFetcher;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.format.TextDecoration;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+// Paper end - version command 2.0
 
 public class VersionCommand extends BukkitCommand {
+    private VersionFetcher versionFetcher; // Paper - version command 2.0
+    private VersionFetcher getVersionFetcher() { // lazy load because unsafe isn't available at command registration
+        if (versionFetcher == null) {
+            versionFetcher = Bukkit.getUnsafe().getVersionFetcher();
+        }
+
+        return versionFetcher;
+    }
+
     public VersionCommand(@NotNull String name) {
         super(name);
 
@@ -41,7 +58,7 @@ public class VersionCommand extends BukkitCommand {
         if (!testPermission(sender)) return true;
 
         if (args.length == 0) {
-            sender.sendMessage("This server is running " + Bukkit.getName() + " version " + Bukkit.getVersion() + " (Implementing API version " + Bukkit.getBukkitVersion() + ")");
+            //sender.sendMessage("This server is running " + Bukkit.getName() + " version " + Bukkit.getVersion() + " (Implementing API version " + Bukkit.getBukkitVersion() + ")"); // Paper - moved to setVersionMessage
             sendVersion(sender);
         } else {
             StringBuilder name = new StringBuilder();
@@ -80,8 +97,17 @@ public class VersionCommand extends BukkitCommand {
 
     private void describeToSender(@NotNull Plugin plugin, @NotNull CommandSender sender) {
         PluginDescriptionFile desc = plugin.getDescription();
-        sender.sendMessage(ChatColor.GREEN + desc.getName() + ChatColor.WHITE + " version " + ChatColor.GREEN + desc.getVersion());
-
+        // Paper start - version command 2.0
+        sender.sendMessage(
+            Component.text()
+                .append(Component.text(desc.getName(), NamedTextColor.GREEN))
+                .append(Component.text(" version "))
+                .append(Component.text(desc.getVersion(), NamedTextColor.GREEN)
+                    .hoverEvent(Component.text("Click to copy to clipboard", NamedTextColor.WHITE))
+                    .clickEvent(ClickEvent.copyToClipboard(desc.getVersion()))
+                )
+        );
+        // Paper end - version command 2.0
         if (desc.getDescription() != null) {
             sender.sendMessage(desc.getDescription());
         }
@@ -147,14 +173,14 @@ public class VersionCommand extends BukkitCommand {
 
     private final ReentrantLock versionLock = new ReentrantLock();
     private boolean hasVersion = false;
-    private String versionMessage = null;
+    private Component versionMessage = null; // Paper
     private final Set<CommandSender> versionWaiters = new HashSet<CommandSender>();
     private boolean versionTaskStarted = false;
     private long lastCheck = 0;
 
     private void sendVersion(@NotNull CommandSender sender) {
         if (hasVersion) {
-            if (System.currentTimeMillis() - lastCheck > 21600000) {
+            if (System.currentTimeMillis() - lastCheck > getVersionFetcher().getCacheTime()) { // Paper - use version supplier
                 lastCheck = System.currentTimeMillis();
                 hasVersion = false;
             } else {
@@ -169,7 +195,7 @@ public class VersionCommand extends BukkitCommand {
                 return;
             }
             versionWaiters.add(sender);
-            sender.sendMessage("Checking version, please wait...");
+            sender.sendMessage(Component.text("Checking version, please wait...", NamedTextColor.WHITE, TextDecoration.ITALIC)); // Paper
             if (!versionTaskStarted) {
                 versionTaskStarted = true;
                 new Thread(new Runnable() {
@@ -187,6 +213,13 @@ public class VersionCommand extends BukkitCommand {
 
     private void obtainVersion() {
         String version = Bukkit.getVersion();
+        // Paper start
+        if (version.startsWith("null")) { // running from ide?
+            setVersionMessage(Component.text("Unknown version, custom build?", NamedTextColor.YELLOW));
+            return;
+        }
+        setVersionMessage(getVersionFetcher().getVersionMessage(version));
+        /*
         if (version == null) version = "Custom";
         String[] parts = version.substring(0, version.indexOf(' ')).split("-");
         if (parts.length == 4) {
@@ -216,11 +249,24 @@ public class VersionCommand extends BukkitCommand {
         } else {
             setVersionMessage("Unknown version, custom build?");
         }
+         */
+        // Paper end
     }
 
-    private void setVersionMessage(@NotNull String msg) {
+    // Paper start
+    private void setVersionMessage(final @NotNull Component msg) {
         lastCheck = System.currentTimeMillis();
-        versionMessage = msg;
+        final Component message = Component.textOfChildren(
+            Component.text(Bukkit.getVersionMessage(), NamedTextColor.WHITE),
+            Component.newline(),
+            msg
+        );
+        this.versionMessage = Component.text()
+            .append(message)
+            .hoverEvent(Component.text("Click to copy to clipboard", NamedTextColor.WHITE))
+            .clickEvent(ClickEvent.copyToClipboard(PlainTextComponentSerializer.plainText().serialize(message)))
+            .build();
+        // Paper end
         versionLock.lock();
         try {
             hasVersion = true;
diff --git a/src/test/java/io/papermc/paper/TestServerBuildInfo.java b/src/test/java/io/papermc/paper/TestServerBuildInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..17be27a869c1047a7a9440fb8f3717260d4abbd0
--- /dev/null
+++ b/src/test/java/io/papermc/paper/TestServerBuildInfo.java
@@ -0,0 +1,59 @@
+package io.papermc.paper;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.OptionalInt;
+import net.kyori.adventure.key.Key;
+import org.jetbrains.annotations.NotNull;
+
+public class TestServerBuildInfo implements ServerBuildInfo {
+    @Override
+    public @NotNull Key brandId() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isBrandCompatible(final @NotNull Key brandId) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull String brandName() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull String minecraftVersionId() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull String minecraftVersionName() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull OptionalInt buildNumber() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull Instant buildTime() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull Optional<String> gitBranch() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull Optional<String> gitCommit() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull String asString(final @NotNull StringRepresentation representation) {
+        return "";
+    }
+}
diff --git a/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo b/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo
new file mode 100644
index 0000000000000000000000000000000000000000..64e2f8559b9c5a52e0a3229d3d12f65e9af145b3
--- /dev/null
+++ b/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo
@@ -0,0 +1 @@
+io.papermc.paper.TestServerBuildInfo