From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Paul Sauve <paul@technove.co>
Date: Sun, 20 Dec 2020 14:08:24 -0600
Subject: [PATCH] Airplane Profiler

Airplane
Copyright (C) 2020 Technove LLC

This program is 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.

This program 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/>.

diff --git a/pom.xml b/pom.xml
index 752d62eb3b87ab24260ec2c029bae0d2b0e3b908..02bbe784d0a062dd643e37f3ad04b77a78144828 100644
--- a/pom.xml
+++ b/pom.xml
@@ -161,6 +161,13 @@
             <version>1.1.0-SNAPSHOT</version>
             <scope>compile</scope>
         </dependency>
+        <!-- Airplane - Flare -->
+        <dependency>
+            <groupId>com.github.technove</groupId>
+            <artifactId>Flare</artifactId>
+            <version>master-SNAPSHOT</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
     <repositories>
@@ -174,6 +181,11 @@
             <id>spigotmc-public</id>
             <url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
         </repository>
+        <!-- Airplane - Flare -->
+        <repository>
+            <id>jitpack.io</id>
+            <url>https://jitpack.io</url>
+        </repository>
     </repositories>
 
     <pluginRepositories>
diff --git a/src/main/java/gg/airplane/AirplaneConfig.java b/src/main/java/gg/airplane/AirplaneConfig.java
index c39c9929a251b41ab60e1509b4e6c90d0825dc0c..3a5a65a2f282b00602643470520fd05fba5907fb 100644
--- a/src/main/java/gg/airplane/AirplaneConfig.java
+++ b/src/main/java/gg/airplane/AirplaneConfig.java
@@ -68,4 +68,23 @@ public class AirplaneConfig {
         dynamicHoglinBehavior = manual.get("behavior-activation.hoglin", dynamicHoglinBehavior);
     }
 
+
+    public static String profileWebUrl = "https://flare.airplane.gg";
+
+    private static void profilerOptions() {
+        manual.setComment("flare", "Configures Flare, the built-in profiler");
+
+        profileWebUrl = manual.get("flare.url", profileWebUrl, "Sets the server to use for profiles.");
+    }
+
+
+    public static String accessToken = "";
+
+    private static void airplaneWebServices() {
+        manual.setComment("web-services", "Options for connecting to Airplane's online utilities");
+
+        accessToken = manual.get("web-services.token", "");
+        // todo lookup token (off-thread) and let users know if their token is valid
+    }
+
 }
diff --git a/src/main/java/gg/airplane/AirplaneLogger.java b/src/main/java/gg/airplane/AirplaneLogger.java
new file mode 100644
index 0000000000000000000000000000000000000000..1a9d71739019d12772bec6076b195552ff6299f9
--- /dev/null
+++ b/src/main/java/gg/airplane/AirplaneLogger.java
@@ -0,0 +1,17 @@
+package gg.airplane;
+
+import org.bukkit.Bukkit;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class AirplaneLogger extends Logger {
+    public static final AirplaneLogger LOGGER = new AirplaneLogger();
+
+    private AirplaneLogger() {
+        super("Airplane", null);
+
+        setParent(Bukkit.getLogger());
+        setLevel(Level.ALL);
+    }
+}
diff --git a/src/main/java/gg/airplane/commands/AirplaneCommands.java b/src/main/java/gg/airplane/commands/AirplaneCommands.java
index 807cf274619b8f7be839e249cb62b9817876ca04..66b20250a26d005427601b1cdee43bdd9eba70cc 100644
--- a/src/main/java/gg/airplane/commands/AirplaneCommands.java
+++ b/src/main/java/gg/airplane/commands/AirplaneCommands.java
@@ -1,10 +1,12 @@
 package gg.airplane.commands;
 
 import gg.airplane.AirplaneCommand;
+import gg.airplane.flare.FlareCommand;
 import net.minecraft.server.MinecraftServer;
 
 public class AirplaneCommands {
     public static void init() {
         MinecraftServer.getServer().server.getCommandMap().register("airplane", "Airplane", new AirplaneCommand());
+        MinecraftServer.getServer().server.getCommandMap().register("flare", "Airplane", new FlareCommand());
     }
 }
diff --git a/src/main/java/gg/airplane/compat/ServerConfigurations.java b/src/main/java/gg/airplane/compat/ServerConfigurations.java
new file mode 100644
index 0000000000000000000000000000000000000000..f4976428bc721319d2926e97cbe0f64c6e9e503c
--- /dev/null
+++ b/src/main/java/gg/airplane/compat/ServerConfigurations.java
@@ -0,0 +1,77 @@
+package gg.airplane.compat;
+
+import co.aikar.timings.TimingsManager;
+import com.google.common.io.Files;
+import org.bukkit.configuration.InvalidConfigurationException;
+import org.bukkit.configuration.file.YamlConfiguration;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import java.util.stream.Collectors;
+
+public class ServerConfigurations {
+
+    public static final String[] configurationFiles = new String[]{
+      "server.properties",
+      "bukkit.yml",
+      "spigot.yml",
+      "paper.yml",
+      "tuinity.yml",
+      "airplane.air"
+    };
+
+    public static String getCleanCopy(String configName) throws IOException {
+        File file = new File(configName);
+        List<String> hiddenConfigs = TimingsManager.hiddenConfigs;
+
+        if (configName.equals("airplane.air")) {
+            return Files.readLines(file, StandardCharsets.UTF_8)
+              .stream()
+              .filter(line -> !line.trim().startsWith("#"))
+              .map(line -> line.contains("token") ? "  token = **" : line)
+              .collect(Collectors.joining("\n"));
+        }
+
+        switch (Files.getFileExtension(configName)) {
+            case "properties": {
+                Properties properties = new Properties();
+                try (FileInputStream inputStream = new FileInputStream(file)) {
+                    properties.load(inputStream);
+                }
+                for (String hiddenConfig : hiddenConfigs) {
+                    properties.remove(hiddenConfig);
+                }
+                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+                properties.store(outputStream, "");
+                return Arrays.stream(outputStream.toString()
+                  .split("\n"))
+                  .filter(line -> !line.startsWith("#"))
+                  .collect(Collectors.joining("\n"));
+            }
+            case "yml": {
+                YamlConfiguration configuration = new YamlConfiguration();
+                try {
+                    configuration.load(file);
+                } catch (InvalidConfigurationException e) {
+                    throw new IOException(e);
+                }
+                configuration.options().header(null);
+                for (String key : configuration.getKeys(true)) {
+                    if (hiddenConfigs.contains(key)) {
+                        configuration.set(key, null);
+                    }
+                }
+                return configuration.saveToString();
+            }
+            default:
+                throw new IllegalArgumentException("Bad file type " + configName);
+        }
+    }
+
+}
diff --git a/src/main/java/gg/airplane/flare/FlareCommand.java b/src/main/java/gg/airplane/flare/FlareCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..81f5267d287138f6b47aa0a1f1d59d2d91a7e34a
--- /dev/null
+++ b/src/main/java/gg/airplane/flare/FlareCommand.java
@@ -0,0 +1,158 @@
+package gg.airplane.flare;
+
+import com.google.common.collect.ImmutableList;
+import gg.airplane.AirplaneConfig;
+import gg.airplane.flare.exceptions.UserReportableException;
+import gg.airplane.flare.profiling.AsyncProfilerIntegration;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+import net.md_5.bungee.api.chat.TextComponent;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.ConsoleCommandSender;
+import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin;
+import org.bukkit.util.StringUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class FlareCommand extends Command {
+
+    private static final ChatColor HEX = ChatColor.of("#6a7eda");
+    private static final BaseComponent[] PREFIX = new ComponentBuilder("Flare ✈ ")
+      .color(HEX)
+      .bold(true)
+      .create();
+
+    private static ComponentBuilder create(ComponentBuilder newBuilder) {
+        newBuilder.color(HEX);
+        newBuilder.bold(false);
+
+        ComponentBuilder builder = new ComponentBuilder();
+        builder.append(PREFIX);
+        builder.append(newBuilder.create());
+        return builder;
+    }
+
+    public FlareCommand() {
+        super("flare", "Profile your server with Flare", "/flare", Collections.singletonList("profile"));
+        this.setPermission("airplane.flare");
+    }
+
+    @Override
+    public boolean execute(CommandSender sender, String commandLabel, String[] args) {
+        if (!testPermission(sender)) return true;
+        if (AirplaneConfig.accessToken.length() == 0) {
+            BaseComponent clickable = new TextComponent("https://blog.airplane.gg/flare");
+            clickable.setUnderlined(true);
+            clickable.setColor(HEX);
+            clickable.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://blog.airplane.gg/flare"));
+
+            sender.sendMessage(create(new ComponentBuilder("Flare currently requires an access token to use. To learn more, visit ")
+              .color(HEX)
+              .append(clickable)
+            )
+              .create());
+            return true;
+        }
+
+        if (AsyncProfilerIntegration.doesNotSupportProfiling()) {
+            sender.sendMessage(create(new ComponentBuilder("Profiling is not supported in this environment, reason: " + AsyncProfilerIntegration.getDisabledReason())
+              .color(ChatColor.RED)
+            ).create());
+            return true;
+        }
+        if (ProfilingManager.isProfiling()) {
+            if (args.length == 1 && args[0].equalsIgnoreCase("status")) {
+                sender.sendMessage(create(new ComponentBuilder("Status: " + AsyncProfilerIntegration.status())).create());
+                return true;
+            }
+            if (ProfilingManager.stop()) {
+                if (!(sender instanceof ConsoleCommandSender)) {
+                    sender.sendMessage(create(new ComponentBuilder("Profiling has been stopped.")).create());
+                }
+            } else {
+                sender.sendMessage(create(new ComponentBuilder("Profiling has already been stopped.")).create());
+            }
+        } else {
+            ProfileType profileType = null;
+            if (args.length > 0) {
+                try {
+                    profileType = ProfileType.valueOf(args[0].toUpperCase());
+                } catch (Exception e) {
+                    sender.sendMessage(create(new ComponentBuilder("Invalid profile type ")
+                      .append(args[0]).color(ChatColor.BOLD)
+                      .append("!").color(HEX)).create());
+                    return false;
+                }
+            }
+            int interval = 5;
+            if (args.length > 1) {
+                try {
+                    interval = Integer.parseInt(args[1]);
+                } catch (Exception e) {
+                    sender.sendMessage(create(new ComponentBuilder("Invalid time in milliseconds ")
+                      .append(args[1]).color(HEX).bold(true)
+                      .append("!").color(HEX)).create());
+                    return false;
+                }
+            }
+            int finalInterval = interval;
+            ProfileType finalProfileType = profileType;
+            Bukkit.getScheduler().runTaskAsynchronously(new MinecraftInternalPlugin(), () -> {
+                try {
+                    if (ProfilingManager.start(finalProfileType, finalInterval)) {
+                        if (!(sender instanceof ConsoleCommandSender)) {
+                            sender.sendMessage(create(new ComponentBuilder("Profiling has been started: " + ProfilingManager.getProfilingUrl().get())
+                              .event(new ClickEvent(ClickEvent.Action.OPEN_URL, ProfilingManager.getProfilingUrl().get())))
+                              .create());
+                            sender.sendMessage(create(new ComponentBuilder("  Run /" + commandLabel + " to stop profiling.")).create());
+                        }
+                    } else {
+                        sender.sendMessage(create(new ComponentBuilder("Profiling has already been started: " + ProfilingManager.getProfilingUrl().get())
+                          .event(new ClickEvent(ClickEvent.Action.OPEN_URL, ProfilingManager.getProfilingUrl().get())))
+                          .create());
+                    }
+                } catch (UserReportableException e) {
+                    sender.sendMessage(create(new ComponentBuilder("Profiling failed to start: " + e.getUserError())).create());
+                    e.printStackTrace();
+                }
+            });
+        }
+        return true;
+    }
+
+    @Override
+    public List<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
+        if (AsyncProfilerIntegration.doesNotSupportProfiling()) {
+            return ImmutableList.of();
+        }
+
+        List<String> list = new ArrayList<>();
+        if (AsyncProfilerIntegration.isProfiling()) {
+            if (args.length == 1) {
+                String lastWord = args[0];
+                if (StringUtil.startsWithIgnoreCase("status", lastWord)) {
+                    list.add("status");
+                }
+                if (StringUtil.startsWithIgnoreCase("stop", lastWord)) {
+                    list.add("stop");
+                }
+            }
+        } else {
+            if (args.length <= 1) {
+                String lastWord = args.length == 0 ? "" : args[0];
+                for (ProfileType value : ProfileType.values()) {
+                    if (StringUtil.startsWithIgnoreCase(value.getInternalName(), lastWord)) {
+                        list.add(value.name().toLowerCase());
+                    }
+                }
+            }
+        }
+        return list;
+    }
+}
diff --git a/src/main/java/gg/airplane/flare/FlareSetup.java b/src/main/java/gg/airplane/flare/FlareSetup.java
new file mode 100644
index 0000000000000000000000000000000000000000..27ac32779e700494aeca8b425edb2871d3ec29cc
--- /dev/null
+++ b/src/main/java/gg/airplane/flare/FlareSetup.java
@@ -0,0 +1,146 @@
+package gg.airplane.flare;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import gg.airplane.AirplaneConfig;
+import gg.airplane.AirplaneLogger;
+import gg.airplane.compat.ServerConfigurations;
+import gg.airplane.flare.profiling.AsyncProfilerIntegration;
+import net.minecraft.server.MinecraftServer;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.PluginClassLoader;
+import org.bukkit.scheduler.BukkitTask;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+
+public class FlareSetup {
+
+    public static void init() {
+        ServerConnector.connector = new ServerConnector() {
+
+            private final Cache<String, String> pluginNameCache = CacheBuilder.newBuilder()
+              .expireAfterAccess(1, TimeUnit.MINUTES)
+              .maximumSize(1024)
+              .build();
+
+            @Override
+            public String getPluginForClass(String name) {
+                if (name.contains(".") && name.charAt(0) != '/') {
+                    if (name.startsWith("net.minecraft") || name.startsWith("java.") || name.startsWith("com.mojang") || name.startsWith("com.google") || name.startsWith("it.unimi") || name.startsWith("sun")) {
+                        return null;
+                    }
+
+                    String className = name.substring(0, name.lastIndexOf("."));
+                    String existing = pluginNameCache.getIfPresent(name);
+                    if (existing != null) {
+                        return existing.isEmpty() ? null : existing;
+                    }
+
+                    String newValue = "";
+
+                    for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
+                        ClassLoader classLoader = plugin.getClass().getClassLoader();
+                        if (classLoader instanceof PluginClassLoader) {
+                            try {
+                                Class<?> aClass = ((PluginClassLoader) classLoader)._airplane_findClass(className);
+                                if (aClass != null) {
+                                    newValue = plugin.getName();
+                                }
+                            } catch (ClassNotFoundException | IllegalAccessError e) {
+                            }
+                        }
+                    }
+
+                    pluginNameCache.put(name, newValue);
+                }
+                return null;
+            }
+
+            @Override
+            public Thread getMainThread() {
+                return MinecraftServer.getServer().serverThread;
+            }
+
+            @Override
+            public Map<String, String> getConfigurations() {
+                Map<String, String> map = new LinkedHashMap<>();
+                for (String configurationFile : ServerConfigurations.configurationFiles) {
+                    try {
+                        map.put(configurationFile, ServerConfigurations.getCleanCopy(configurationFile));
+                    } catch (IOException e) {
+                        this.log(Level.WARNING, "Failed to load config file " + configurationFile, e);
+                    }
+                }
+                return map;
+            }
+
+            @Override
+            public void log(Level level, String s) {
+                AirplaneLogger.LOGGER.log(level, s);
+            }
+
+            @Override
+            public void log(Level level, String s, Throwable throwable) {
+                AirplaneLogger.LOGGER.log(level, s, throwable);
+            }
+
+            @Override
+            public String getPrimaryVersion() {
+                return Bukkit.getVersion();
+            }
+
+            @Override
+            public String getApiVersion() {
+                return "bukkit:" + Bukkit.getBukkitVersion();
+            }
+
+            @Override
+            public String getMcVersion() {
+                return Bukkit.getMinecraftVersion();
+            }
+
+            private final Map<Runnable, BukkitTask> scheduledRunnables = new HashMap<>();
+
+            @Override
+            public void schedule(Runnable runnable, long l, long l1) {
+                BukkitTask task = Bukkit.getScheduler().runTaskTimer(new MinecraftInternalPlugin(), runnable, l, l1);
+                this.scheduledRunnables.put(runnable, task);
+            }
+
+            @Override
+            public void scheduleAsync(Runnable runnable, long l, long l1) {
+                BukkitTask task = Bukkit.getScheduler().runTaskTimerAsynchronously(new MinecraftInternalPlugin(), runnable, l, l1);
+                this.scheduledRunnables.put(runnable, task);
+            }
+
+            @Override
+            public void cancel(Runnable runnable) {
+                this.scheduledRunnables.get(runnable).cancel();
+            }
+
+            @Override
+            public void runAsync(Runnable runnable) {
+                Bukkit.getScheduler().runTaskAsynchronously(new MinecraftInternalPlugin(), runnable);
+            }
+
+            @Override
+            public String getWebUrl() {
+                return AirplaneConfig.profileWebUrl;
+            }
+
+            @Override
+            public String getToken() {
+                return AirplaneConfig.accessToken;
+            }
+        };
+        AsyncProfilerIntegration.init();
+    }
+
+}
diff --git a/src/main/java/gg/airplane/flare/ProfilingManager.java b/src/main/java/gg/airplane/flare/ProfilingManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..86d6650d174a7794a7ebe793cad033b42215c321
--- /dev/null
+++ b/src/main/java/gg/airplane/flare/ProfilingManager.java
@@ -0,0 +1,65 @@
+package gg.airplane.flare;
+
+import gg.airplane.AirplaneConfig;
+import gg.airplane.AirplaneLogger;
+import gg.airplane.flare.exceptions.UserReportableException;
+import gg.airplane.flare.profiling.ProfileController;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin;
+import org.bukkit.scheduler.BukkitTask;
+
+import javax.annotation.Nullable;
+import java.util.Optional;
+import java.util.logging.Level;
+
+public class ProfilingManager {
+
+    private static ProfileController currentController;
+    private static BukkitTask currentTask = null;
+
+    public static synchronized boolean isProfiling() {
+        return currentController != null;
+    }
+
+    public static synchronized Optional<String> getProfilingUrl() {
+        if (!isProfiling()) {
+            return Optional.empty();
+        }
+        return Optional.of(AirplaneConfig.profileWebUrl + "/" + currentController.getId());
+    }
+
+    public static synchronized boolean start(@Nullable ProfileType type, int interval) throws UserReportableException {
+        if (isProfiling()) {
+            return false;
+        }
+        if (Bukkit.isPrimaryThread()) {
+            throw new UserReportableException("Profiles should be started off-thread");
+        }
+        currentController = new ProfileController(type, Math.max(interval, 1)); // don't allow lower than 20ms: https://bugzilla.redhat.com/show_bug.cgi?id=645528
+        currentTask = Bukkit.getScheduler().runTaskLater(new MinecraftInternalPlugin(), ProfilingManager::stop, 20 * 60 * 15);
+        AirplaneLogger.LOGGER.log(Level.INFO, "Flare has been started: " + getProfilingUrl().orElse("An error occurred retrieving the Flare URL."));
+        return true;
+    }
+
+    public static synchronized boolean stop() {
+        if (!isProfiling()) {
+            return false;
+        }
+        AirplaneLogger.LOGGER.log(Level.INFO, "Flare has been stopped: " + getProfilingUrl().orElse("An error occurred retrieving the Flare URL."));
+        try {
+            currentController.cancel();
+        } catch (Throwable t) {
+            AirplaneLogger.LOGGER.log(Level.WARNING, "Error occurred stopping Flare", t);
+        }
+        currentController = null;
+
+        try {
+            currentTask.cancel();
+        } catch (Throwable t) {
+            AirplaneLogger.LOGGER.log(Level.WARNING, "Error occurred stopping Flare", t);
+        }
+        currentTask = null;
+        return true;
+    }
+
+}
diff --git a/src/main/java/net/minecraft/server/DedicatedServer.java b/src/main/java/net/minecraft/server/DedicatedServer.java
index b8a45c80b6691214e513286262f31b7b5b1dd5eb..a85a844a4766cde16eea9e814abfec29d8935c0f 100644
--- a/src/main/java/net/minecraft/server/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/DedicatedServer.java
@@ -187,6 +187,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer
         com.tuinity.tuinity.config.TuinityConfig.init((java.io.File) options.valueOf("tuinity-settings")); // Tuinity - Server Config
         gg.airplane.AirplaneConfig.load(); // Airplane - config
         gg.airplane.commands.AirplaneCommands.init(); // Airplane - command
+        gg.airplane.flare.FlareSetup.init(); // Airplane
 
         this.setPVP(dedicatedserverproperties.pvp);
         this.setAllowFlight(dedicatedserverproperties.allowFlight);
diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
index 49dc0c441b9dd7e7745cf15ced67f383ebee1f99..b343d8ee7435312929558efdaf127334d8e2fff6 100644
--- a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
@@ -19,7 +19,8 @@ public class MinecraftInternalPlugin extends PluginBase {
     private boolean enabled = true;
 
     private final String pluginName;
-    private PluginDescriptionFile pdf;
+    private org.bukkit.plugin.PluginLogger logger;
+    private PluginDescriptionFile pdf; // Airplane
 
     public MinecraftInternalPlugin() {
         this.pluginName = "Minecraft";
@@ -72,7 +73,12 @@ public class MinecraftInternalPlugin extends PluginBase {
 
     @Override
     public PluginLogger getLogger() {
-        throw new UnsupportedOperationException("Not supported.");
+        // Airplane start
+        if (this.logger == null) {
+            this.logger = new org.bukkit.plugin.PluginLogger(this); // Airplane
+        }
+        return this.logger;
+        // Airplane end
     }
 
     @Override
@@ -82,7 +88,7 @@ public class MinecraftInternalPlugin extends PluginBase {
 
     @Override
     public Server getServer() {
-        throw new UnsupportedOperationException("Not supported.");
+        return org.bukkit.Bukkit.getServer(); // Airplane - impl
     }
 
     @Override
