// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.core.plugin;

import java.util.Enumeration;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
import java.util.Set;
import com.hypixel.hytale.event.IEventDispatcher;
import com.hypixel.hytale.server.core.plugin.event.PluginSetupEvent;
import com.hypixel.hytale.server.core.asset.AssetModule;
import com.hypixel.hytale.assetstore.AssetStore;
import java.net.URLClassLoader;
import com.hypixel.hytale.assetstore.AssetRegistry;
import java.net.URLConnection;
import java.net.URI;
import java.net.JarURLConnection;
import java.util.HashSet;
import java.util.Collections;
import java.io.InputStream;
import java.net.MalformedURLException;
import com.hypixel.hytale.codec.ExtraInfo;
import java.io.Reader;
import com.hypixel.hytale.codec.util.RawJsonReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.io.IOException;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import com.hypixel.hytale.common.semver.Semver;
import com.hypixel.hytale.common.util.java.ManifestUtil;
import java.util.Collection;
import com.hypixel.hytale.common.semver.SemverRange;
import com.hypixel.hytale.server.core.ShutdownReason;
import java.util.Iterator;
import java.util.concurrent.CompletableFuture;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import joptsimple.OptionSpec;
import com.hypixel.hytale.server.core.Options;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.HashMap;
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.server.core.plugin.commands.PluginCommand;
import com.hypixel.hytale.server.core.command.system.CommandManager;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.HytaleServer;
import com.hypixel.hytale.server.core.HytaleServerConfig;
import com.hypixel.hytale.server.core.plugin.pending.PendingLoadJavaPlugin;
import java.util.concurrent.ConcurrentHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.net.URL;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.common.plugin.PluginManifest;
import javax.annotation.Nullable;
import com.hypixel.hytale.common.plugin.PluginIdentifier;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.hypixel.hytale.server.core.plugin.pending.PendingLoadPlugin;
import java.util.List;
import com.hypixel.hytale.metrics.MetricsRegistry;
import java.nio.file.Path;
import javax.annotation.Nonnull;
import com.hypixel.hytale.logger.HytaleLogger;

public class PluginManager
{
    @Nonnull
    private static final HytaleLogger LOGGER;
    @Nonnull
    public static final Path MODS_PATH;
    @Nonnull
    public static final MetricsRegistry<PluginManager> METRICS_REGISTRY;
    private static PluginManager instance;
    @Nonnull
    private final PluginClassLoader corePluginClassLoader;
    @Nonnull
    private final List<PendingLoadPlugin> corePlugins;
    private final PluginBridgeClassLoader bridgeClassLoader;
    private final ReentrantReadWriteLock lock;
    private final Map<PluginIdentifier, PluginBase> plugins;
    private final Map<Path, PluginClassLoader> classLoaders;
    private final boolean loadExternalPlugins = true;
    @Nonnull
    private PluginState state;
    @Nullable
    private List<PendingLoadPlugin> loadOrder;
    @Nullable
    private Map<PluginIdentifier, PluginBase> loading;
    @Nonnull
    private final Map<PluginIdentifier, PluginManifest> availablePlugins;
    public PluginListPageManager pluginListPageManager;
    private ComponentType<EntityStore, PluginListPageManager.SessionSettings> sessionSettingsComponentType;
    
    public static PluginManager get() {
        return PluginManager.instance;
    }
    
    public PluginManager() {
        this.corePluginClassLoader = new PluginClassLoader(this, true, new URL[0]);
        this.corePlugins = new ObjectArrayList<PendingLoadPlugin>();
        this.bridgeClassLoader = new PluginBridgeClassLoader(this, PluginManager.class.getClassLoader());
        this.lock = new ReentrantReadWriteLock();
        this.plugins = new Object2ObjectLinkedOpenHashMap<PluginIdentifier, PluginBase>();
        this.classLoaders = new ConcurrentHashMap<Path, PluginClassLoader>();
        this.state = PluginState.NONE;
        this.availablePlugins = new Object2ObjectLinkedOpenHashMap<PluginIdentifier, PluginManifest>();
        PluginManager.instance = this;
        this.pluginListPageManager = new PluginListPageManager();
    }
    
    public void registerCorePlugin(@Nonnull final PluginManifest builder) {
        this.corePlugins.add(new PendingLoadJavaPlugin(null, builder, this.corePluginClassLoader));
    }
    
    private boolean canLoadOnBoot(@Nonnull final PluginManifest manifest) {
        final PluginIdentifier identifier = new PluginIdentifier(manifest);
        final HytaleServerConfig.ModConfig modConfig = HytaleServer.get().getConfig().getModConfig().get(identifier);
        boolean enabled;
        if (modConfig == null || modConfig.getEnabled() == null) {
            enabled = !manifest.isDisabledByDefault();
        }
        else {
            enabled = modConfig.getEnabled();
        }
        if (enabled) {
            return true;
        }
        PluginManager.LOGGER.at(Level.WARNING).log("Skipping mod %s (Disabled by server config)", identifier);
        return false;
    }
    
    public void setup() {
        if (this.state != PluginState.NONE) {
            throw new IllegalStateException("Expected PluginState.NONE but found " + String.valueOf(this.state));
        }
        this.state = PluginState.SETUP;
        CommandManager.get().registerSystemCommand(new PluginCommand());
        this.sessionSettingsComponentType = EntityStore.REGISTRY.registerComponent(PluginListPageManager.SessionSettings.class, PluginListPageManager.SessionSettings::new);
        final HashMap<PluginIdentifier, PendingLoadPlugin> pending = new HashMap<PluginIdentifier, PendingLoadPlugin>();
        this.availablePlugins.clear();
        PluginManager.LOGGER.at(Level.INFO).log("Loading pending core plugins!");
        for (int i = 0; i < this.corePlugins.size(); ++i) {
            final PendingLoadPlugin plugin = this.corePlugins.get(i);
            PluginManager.LOGGER.at(Level.INFO).log("- %s", plugin.getIdentifier());
            if (this.canLoadOnBoot(plugin.getManifest())) {
                loadPendingPlugin(pending, plugin);
            }
            else {
                this.availablePlugins.put(plugin.getIdentifier(), plugin.getManifest());
            }
        }
        Path self;
        try {
            self = Paths.get(PluginManager.class.getProtectionDomain().getCodeSource().getLocation().toURI());
        }
        catch (final URISyntaxException e) {
            throw new RuntimeException(e);
        }
        this.loadPluginsFromDirectory(pending, self.getParent().resolve("builtin"), false, this.availablePlugins);
        this.loadPluginsInClasspath(pending, this.availablePlugins);
        this.loadPluginsFromDirectory(pending, PluginManager.MODS_PATH, !Options.getOptionSet().has(Options.BARE), this.availablePlugins);
        for (final Path modsPath : Options.getOptionSet().valuesOf(Options.MODS_DIRECTORIES)) {
            this.loadPluginsFromDirectory(pending, modsPath, false, this.availablePlugins);
        }
        this.lock.readLock().lock();
        try {
            this.plugins.keySet().forEach(key -> {
                pending.remove(key);
                PluginManager.LOGGER.at(Level.WARNING).log("Skipping loading of %s because it is already loaded!", key);
                return;
            });
            final Iterator<PendingLoadPlugin> iterator = pending.values().iterator();
            while (iterator.hasNext()) {
                final PendingLoadPlugin pendingLoadPlugin = iterator.next();
                try {
                    this.validatePluginDeps(pendingLoadPlugin, pending);
                }
                catch (final MissingPluginDependencyException e2) {
                    PluginManager.LOGGER.at(Level.SEVERE).log(e2.getMessage());
                    iterator.remove();
                }
            }
        }
        finally {
            this.lock.readLock().unlock();
        }
        this.loadOrder = PendingLoadPlugin.calculateLoadOrder(pending);
        this.loading = new Object2ObjectOpenHashMap<PluginIdentifier, PluginBase>();
        pending.forEach((identifier, pendingLoad) -> this.availablePlugins.put(identifier, pendingLoad.getManifest()));
        final ObjectArrayList<CompletableFuture<Void>> preLoadFutures = new ObjectArrayList<CompletableFuture<Void>>();
        this.lock.writeLock().lock();
        try {
            PluginManager.LOGGER.at(Level.FINE).log("Loading plugins!");
            for (final PendingLoadPlugin pendingLoadPlugin2 : this.loadOrder) {
                PluginManager.LOGGER.at(Level.FINE).log("- %s", pendingLoadPlugin2.getIdentifier());
                final PluginBase plugin2 = pendingLoadPlugin2.load();
                if (plugin2 != null) {
                    this.plugins.put(plugin2.getIdentifier(), plugin2);
                    this.loading.put(plugin2.getIdentifier(), plugin2);
                    final CompletableFuture<Void> future = plugin2.preLoad();
                    if (future == null) {
                        continue;
                    }
                    preLoadFutures.add(future);
                }
            }
        }
        finally {
            this.lock.writeLock().unlock();
        }
        CompletableFuture.allOf((CompletableFuture<?>[])preLoadFutures.toArray(CompletableFuture[]::new)).join();
        for (final PendingLoadPlugin pendingPlugin : this.loadOrder) {
            final PluginBase plugin2 = this.loading.get(pendingPlugin.getIdentifier());
            if (plugin2 == null) {
                continue;
            }
            if (this.setup(plugin2)) {
                continue;
            }
            this.loading.remove(pendingPlugin.getIdentifier());
        }
    }
    
    public void start() {
        if (this.state != PluginState.SETUP) {
            throw new IllegalStateException("Expected PluginState.SETUP but found " + String.valueOf(this.state));
        }
        this.state = PluginState.START;
        for (final PendingLoadPlugin pendingPlugin : this.loadOrder) {
            final PluginBase plugin = this.loading.get(pendingPlugin.getIdentifier());
            if (plugin == null) {
                continue;
            }
            if (this.start(plugin)) {
                continue;
            }
            this.loading.remove(pendingPlugin.getIdentifier());
        }
        this.loadOrder = null;
        this.loading = null;
        final StringBuilder sb = new StringBuilder();
        for (final Map.Entry<PluginIdentifier, HytaleServerConfig.ModConfig> entry : HytaleServer.get().getConfig().getModConfig().entrySet()) {
            final PluginIdentifier identifier = entry.getKey();
            final HytaleServerConfig.ModConfig modConfig = entry.getValue();
            final SemverRange requiredVersion = modConfig.getRequiredVersion();
            if (requiredVersion != null && !this.hasPlugin(identifier, requiredVersion)) {
                sb.append(String.format("%s, Version: %s\n", identifier, modConfig));
                return;
            }
        }
        if (!sb.isEmpty()) {
            final String msg = "Failed to start server! Missing Mods:\n" + String.valueOf(sb);
            PluginManager.LOGGER.at(Level.SEVERE).log(msg);
            HytaleServer.get().shutdownServer(ShutdownReason.MISSING_REQUIRED_PLUGIN.withMessage(msg));
        }
    }
    
    public void shutdown() {
        this.state = PluginState.SHUTDOWN;
        PluginManager.LOGGER.at(Level.INFO).log("Saving plugins config...");
        this.lock.writeLock().lock();
        try {
            final List<PluginBase> list = new ObjectArrayList<PluginBase>(this.plugins.values());
            for (int i = list.size() - 1; i >= 0; --i) {
                final PluginBase plugin = list.get(i);
                if (plugin.getState() == PluginState.ENABLED) {
                    PluginManager.LOGGER.at(Level.FINE).log("Shutting down %s %s", plugin.getType().getDisplayName(), plugin.getIdentifier());
                    plugin.shutdown0(true);
                    HytaleServer.get().doneStop(plugin);
                    PluginManager.LOGGER.at(Level.INFO).log("Shut down plugin %s", plugin.getIdentifier());
                }
            }
            this.plugins.clear();
        }
        finally {
            this.lock.writeLock().unlock();
        }
    }
    
    @Nonnull
    public PluginState getState() {
        return this.state;
    }
    
    @Nonnull
    public PluginBridgeClassLoader getBridgeClassLoader() {
        return this.bridgeClassLoader;
    }
    
    private void validatePluginDeps(@Nonnull final PendingLoadPlugin pendingLoadPlugin, @Nullable final Map<PluginIdentifier, PendingLoadPlugin> pending) {
        final Semver serverVersion = ManifestUtil.getVersion();
        final SemverRange serverVersionRange = pendingLoadPlugin.getManifest().getServerVersion();
        if (serverVersionRange != null && serverVersion != null && !serverVersionRange.satisfies(serverVersion)) {
            throw new MissingPluginDependencyException(String.format("Failed to load '%s' because version of server does not satisfy '%s'! ", pendingLoadPlugin.getIdentifier(), serverVersion));
        }
        for (final Map.Entry<PluginIdentifier, SemverRange> entry : pendingLoadPlugin.getManifest().getDependencies().entrySet()) {
            final PluginIdentifier identifier = entry.getKey();
            PluginManifest dependency = null;
            if (pending != null) {
                final PendingLoadPlugin pendingDependency = pending.get(identifier);
                if (pendingDependency != null) {
                    dependency = pendingDependency.getManifest();
                }
            }
            if (dependency == null) {
                final PluginBase loadedBase = this.plugins.get(identifier);
                if (loadedBase != null) {
                    dependency = loadedBase.getManifest();
                }
            }
            if (dependency == null) {
                throw new MissingPluginDependencyException(String.format("Failed to load '%s' because the dependency '%s' could not be found!", pendingLoadPlugin.getIdentifier(), identifier));
            }
            final SemverRange expectedVersion = entry.getValue();
            if (!dependency.getVersion().satisfies(expectedVersion)) {
                throw new MissingPluginDependencyException(String.format("Failed to load '%s' because version of dependency '%s'(%s) does not satisfy '%s'!", pendingLoadPlugin.getIdentifier(), identifier, dependency.getVersion(), expectedVersion));
            }
        }
    }
    
    private void loadPluginsFromDirectory(@Nonnull final Map<PluginIdentifier, PendingLoadPlugin> pending, @Nonnull final Path path, final boolean create, @Nonnull final Map<PluginIdentifier, PluginManifest> bootRejectMap) {
        if (!Files.isDirectory(path, new LinkOption[0])) {
            if (create) {
                try {
                    Files.createDirectories(path, (FileAttribute<?>[])new FileAttribute[0]);
                }
                catch (final IOException e) {
                    PluginManager.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to create directory: %s", path);
                }
            }
            return;
        }
        PluginManager.LOGGER.at(Level.INFO).log("Loading pending plugins from directory: " + String.valueOf(path));
        try (final DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
            for (final Path file : stream) {
                if (!Files.isRegularFile(file, new LinkOption[0])) {
                    continue;
                }
                if (!file.getFileName().toString().toLowerCase().endsWith(".jar")) {
                    continue;
                }
                final PendingLoadJavaPlugin plugin = this.loadPendingJavaPlugin(file);
                if (plugin == null) {
                    continue;
                }
                assert plugin.getPath() != null;
                PluginManager.LOGGER.at(Level.INFO).log("- %s from path %s", plugin.getIdentifier(), path.relativize(plugin.getPath()));
                if (this.canLoadOnBoot(plugin.getManifest())) {
                    loadPendingPlugin(pending, plugin);
                }
                else {
                    bootRejectMap.put(plugin.getIdentifier(), plugin.getManifest());
                }
            }
        }
        catch (final IOException e) {
            PluginManager.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to find pending plugins from: %s", path);
        }
    }
    
    @Nullable
    private PendingLoadJavaPlugin loadPendingJavaPlugin(@Nonnull final Path file) {
        try {
            final URL url = file.toUri().toURL();
            final PluginClassLoader pluginClassLoader = this.classLoaders.computeIfAbsent(file, path -> {
                new PluginClassLoader(this, false, new URL[] { url });
                return;
            });
            final URL resource = pluginClassLoader.findResource("manifest.json");
            if (resource == null) {
                PluginManager.LOGGER.at(Level.SEVERE).log("Failed to load pending plugin from '%s'. Failed to load manifest file!", file.toString());
                return null;
            }
            try (final InputStream stream = resource.openStream();
                 final InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
                final char[] buffer = RawJsonReader.READ_BUFFER.get();
                final RawJsonReader rawJsonReader = new RawJsonReader(reader, buffer);
                final ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
                final PluginManifest manifest = PluginManifest.CODEC.decodeJson(rawJsonReader, extraInfo);
                extraInfo.getValidationResults().logOrThrowValidatorExceptions(PluginManager.LOGGER);
                return new PendingLoadJavaPlugin(file, manifest, pluginClassLoader);
            }
        }
        catch (final MalformedURLException e) {
            PluginManager.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to load pending plugin from '%s'. Failed to create URLClassLoader!", file.toString());
        }
        catch (final IOException e2) {
            PluginManager.LOGGER.at(Level.SEVERE).withCause(e2).log("Failed to load pending plugin %s. Failed to load manifest file!", file.toString());
        }
        return null;
    }
    
    private void loadPluginsInClasspath(@Nonnull final Map<PluginIdentifier, PendingLoadPlugin> pending, @Nonnull final Map<PluginIdentifier, PluginManifest> rejectedBootList) {
        PluginManager.LOGGER.at(Level.INFO).log("Loading pending classpath plugins!");
        try {
            final URI uri = PluginManager.class.getProtectionDomain().getCodeSource().getLocation().toURI();
            final ClassLoader classLoader = PluginManager.class.getClassLoader();
            try {
                final HashSet<URL> manifestUrls = new HashSet<URL>(Collections.list(classLoader.getResources("manifest.json")));
                for (final URL manifestUrl : manifestUrls) {
                    final URLConnection connection = manifestUrl.openConnection();
                    try (final InputStream stream = connection.getInputStream()) {
                        try (final InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
                            final char[] buffer = RawJsonReader.READ_BUFFER.get();
                            final RawJsonReader rawJsonReader = new RawJsonReader(reader, buffer);
                            final ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
                            final PluginManifest manifest = PluginManifest.CODEC.decodeJson(rawJsonReader, extraInfo);
                            extraInfo.getValidationResults().logOrThrowValidatorExceptions(PluginManager.LOGGER);
                            PendingLoadJavaPlugin plugin;
                            if (connection instanceof final JarURLConnection jarURLConnection) {
                                final URL classpathUrl = jarURLConnection.getJarFileURL();
                                final Path path = Path.of(classpathUrl.toURI());
                                final PluginClassLoader pluginClassLoader = this.classLoaders.computeIfAbsent(path, f -> {
                                    new PluginClassLoader(this, true, new URL[] { classpathUrl });
                                    return;
                                });
                                plugin = new PendingLoadJavaPlugin(path, manifest, pluginClassLoader);
                            }
                            else {
                                final URI pluginUri = manifestUrl.toURI().resolve(".");
                                final Path path = Paths.get(pluginUri);
                                final URL classpathUrl2 = pluginUri.toURL();
                                final URL classpathUrl;
                                final PluginClassLoader pluginClassLoader2 = this.classLoaders.computeIfAbsent(path, f -> {
                                    new PluginClassLoader(this, true, new URL[] { classpathUrl });
                                    return;
                                });
                                plugin = new PendingLoadJavaPlugin(path, manifest, pluginClassLoader2);
                            }
                            PluginManager.LOGGER.at(Level.INFO).log("- %s", plugin.getIdentifier());
                            if (this.canLoadOnBoot(plugin.getManifest())) {
                                loadPendingPlugin(pending, plugin);
                            }
                            else {
                                rejectedBootList.put(plugin.getIdentifier(), plugin.getManifest());
                            }
                        }
                        if (stream == null) {
                            continue;
                        }
                    }
                }
                final URL manifestsUrl = classLoader.getResource("manifests.json");
                if (manifestsUrl != null) {
                    try (final InputStream stream2 = manifestsUrl.openStream();
                         final InputStreamReader reader2 = new InputStreamReader(stream2, StandardCharsets.UTF_8)) {
                        final char[] buffer2 = RawJsonReader.READ_BUFFER.get();
                        final RawJsonReader rawJsonReader2 = new RawJsonReader(reader2, buffer2);
                        final ExtraInfo extraInfo2 = ExtraInfo.THREAD_LOCAL.get();
                        final PluginManifest[] manifests = PluginManifest.ARRAY_CODEC.decodeJson(rawJsonReader2, extraInfo2);
                        extraInfo2.getValidationResults().logOrThrowValidatorExceptions(PluginManager.LOGGER);
                        final URL url = uri.toURL();
                        final Path path2 = Paths.get(uri);
                        final PluginClassLoader pluginClassLoader3 = this.classLoaders.computeIfAbsent(path2, f -> {
                            new PluginClassLoader(this, true, new URL[] { url });
                            return;
                        });
                        for (final PluginManifest manifest2 : manifests) {
                            final PendingLoadJavaPlugin plugin2 = new PendingLoadJavaPlugin(path2, manifest2, pluginClassLoader3);
                            PluginManager.LOGGER.at(Level.INFO).log("- %s", plugin2.getIdentifier());
                            if (this.canLoadOnBoot(plugin2.getManifest())) {
                                loadPendingPlugin(pending, plugin2);
                            }
                            else {
                                rejectedBootList.put(plugin2.getIdentifier(), plugin2.getManifest());
                            }
                        }
                    }
                }
            }
            catch (final IOException e) {
                PluginManager.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to load pending classpath plugin from '%s'. Failed to load manifest file!", uri.toString());
            }
        }
        catch (final URISyntaxException e2) {
            PluginManager.LOGGER.at(Level.SEVERE).withCause(e2).log("Failed to get jar path!");
        }
    }
    
    @Nonnull
    public List<PluginBase> getPlugins() {
        this.lock.readLock().lock();
        try {
            return new ObjectArrayList<PluginBase>(this.plugins.values());
        }
        finally {
            this.lock.readLock().unlock();
        }
    }
    
    @Nullable
    public PluginBase getPlugin(final PluginIdentifier identifier) {
        this.lock.readLock().lock();
        try {
            return this.plugins.get(identifier);
        }
        finally {
            this.lock.readLock().unlock();
        }
    }
    
    public boolean hasPlugin(final PluginIdentifier identifier, @Nonnull final SemverRange range) {
        final PluginBase plugin = this.getPlugin(identifier);
        return plugin != null && plugin.getManifest().getVersion().satisfies(range);
    }
    
    public boolean reload(@Nonnull final PluginIdentifier identifier) {
        final boolean result = this.unload(identifier) && this.load(identifier);
        this.pluginListPageManager.notifyPluginChange(this.plugins, identifier);
        return result;
    }
    
    public boolean unload(@Nonnull final PluginIdentifier identifier) {
        this.lock.writeLock().lock();
        AssetRegistry.ASSET_LOCK.writeLock().lock();
        try {
            final PluginBase plugin = this.plugins.get(identifier);
            if (plugin.getState() == PluginState.ENABLED) {
                plugin.shutdown0(false);
                HytaleServer.get().doneStop(plugin);
                this.plugins.remove(identifier);
                if (plugin instanceof final JavaPlugin javaPlugin) {
                    this.unloadJavaPlugin(javaPlugin);
                }
                this.pluginListPageManager.notifyPluginChange(this.plugins, identifier);
                return true;
            }
            this.pluginListPageManager.notifyPluginChange(this.plugins, identifier);
            return false;
        }
        finally {
            AssetRegistry.ASSET_LOCK.writeLock().unlock();
            this.lock.writeLock().unlock();
        }
    }
    
    protected void unloadJavaPlugin(final JavaPlugin plugin) {
        final Path path = plugin.getFile();
        final PluginClassLoader classLoader = this.classLoaders.remove(path);
        if (classLoader != null) {
            try {
                classLoader.close();
            }
            catch (final IOException e) {
                PluginManager.LOGGER.at(Level.SEVERE).log("Failed to close Class Loader for JavaPlugin %s", plugin.getIdentifier());
            }
        }
    }
    
    public boolean load(@Nonnull final PluginIdentifier identifier) {
        this.lock.readLock().lock();
        try {
            final PluginBase plugin = this.plugins.get(identifier);
            if (plugin != null) {
                this.pluginListPageManager.notifyPluginChange(this.plugins, identifier);
                return false;
            }
        }
        finally {
            this.lock.readLock().unlock();
        }
        final boolean result = this.findAndLoadPlugin(identifier);
        this.pluginListPageManager.notifyPluginChange(this.plugins, identifier);
        return result;
    }
    
    private boolean findAndLoadPlugin(final PluginIdentifier identifier) {
        for (final PendingLoadPlugin plugin : this.corePlugins) {
            if (plugin.getIdentifier().equals(identifier)) {
                return this.load(plugin);
            }
        }
        try {
            final URI uri = PluginManager.class.getProtectionDomain().getCodeSource().getLocation().toURI();
            final ClassLoader classLoader = PluginManager.class.getClassLoader();
            final HashSet<URL> manifestUrls = new HashSet<URL>(Collections.list(classLoader.getResources("manifest.json")));
            for (final URL manifestUrl : manifestUrls) {
                final InputStream stream = manifestUrl.openStream();
                try {
                    final InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8);
                    try {
                        final char[] buffer = RawJsonReader.READ_BUFFER.get();
                        final RawJsonReader rawJsonReader = new RawJsonReader(reader, buffer);
                        final ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
                        final PluginManifest manifest = PluginManifest.CODEC.decodeJson(rawJsonReader, extraInfo);
                        extraInfo.getValidationResults().logOrThrowValidatorExceptions(PluginManager.LOGGER);
                        if (new PluginIdentifier(manifest).equals(identifier)) {
                            final PluginClassLoader pluginClassLoader = new PluginClassLoader(this, true, new URL[] { uri.toURL() });
                            final PendingLoadJavaPlugin plugin2 = new PendingLoadJavaPlugin(Paths.get(uri), manifest, pluginClassLoader);
                            final boolean load = this.load(plugin2);
                            reader.close();
                            if (stream != null) {
                                stream.close();
                            }
                            return load;
                        }
                        reader.close();
                        if (stream != null) {
                            stream.close();
                            continue;
                        }
                        continue;
                    }
                    catch (final Throwable t) {
                        try {
                            reader.close();
                        }
                        catch (final Throwable exception) {
                            t.addSuppressed(exception);
                        }
                        throw t;
                    }
                }
                catch (final Throwable t2) {
                    if (stream != null) {
                        try {
                            stream.close();
                        }
                        catch (final Throwable exception2) {
                            t2.addSuppressed(exception2);
                        }
                    }
                    throw t2;
                }
                break;
            }
            final URL manifestsUrl = classLoader.getResource("manifests.json");
            if (manifestsUrl != null) {
                try (final InputStream stream2 = manifestsUrl.openStream();
                     final InputStreamReader reader2 = new InputStreamReader(stream2, StandardCharsets.UTF_8)) {
                    final char[] buffer2 = RawJsonReader.READ_BUFFER.get();
                    final RawJsonReader rawJsonReader2 = new RawJsonReader(reader2, buffer2);
                    final ExtraInfo extraInfo2 = ExtraInfo.THREAD_LOCAL.get();
                    final PluginManifest[] manifests = PluginManifest.ARRAY_CODEC.decodeJson(rawJsonReader2, extraInfo2);
                    extraInfo2.getValidationResults().logOrThrowValidatorExceptions(PluginManager.LOGGER);
                    for (final PluginManifest manifest2 : manifests) {
                        if (new PluginIdentifier(manifest2).equals(identifier)) {
                            final PluginClassLoader pluginClassLoader2 = new PluginClassLoader(this, true, new URL[] { uri.toURL() });
                            final PendingLoadJavaPlugin plugin3 = new PendingLoadJavaPlugin(Paths.get(uri), manifest2, pluginClassLoader2);
                            final boolean load2 = this.load(plugin3);
                            reader2.close();
                            if (stream2 != null) {
                                stream2.close();
                            }
                            return load2;
                        }
                    }
                }
            }
            final Path path = Paths.get(uri).getParent().resolve("builtin");
            if (Files.exists(path, new LinkOption[0])) {
                try (final DirectoryStream<Path> stream3 = Files.newDirectoryStream(path)) {
                    for (final Path file : stream3) {
                        if (!Files.isRegularFile(file, new LinkOption[0])) {
                            continue;
                        }
                        if (!file.getFileName().toString().toLowerCase().endsWith(".jar")) {
                            continue;
                        }
                        final PluginManifest manifest3 = loadManifest(file);
                        if (manifest3 == null || !new PluginIdentifier(manifest3).equals(identifier)) {
                            continue;
                        }
                        final PendingLoadJavaPlugin pendingLoadJavaPlugin = this.loadPendingJavaPlugin(file);
                        if (pendingLoadJavaPlugin != null) {
                            final boolean load3 = this.load(pendingLoadJavaPlugin);
                            if (stream3 != null) {
                                stream3.close();
                            }
                            return load3;
                        }
                        break;
                    }
                }
                catch (final IOException e) {
                    PluginManager.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to find plugins!");
                }
            }
        }
        catch (final IOException | URISyntaxException e2) {
            PluginManager.LOGGER.at(Level.SEVERE).withCause(e2).log("Failed to load pending classpath plugin. Failed to load manifest file!");
        }
        Boolean result = this.findPluginInDirectory(identifier, PluginManager.MODS_PATH);
        if (result != null) {
            return result;
        }
        for (final Path modsPath : Options.getOptionSet().valuesOf(Options.MODS_DIRECTORIES)) {
            result = this.findPluginInDirectory(identifier, modsPath);
            if (result != null) {
                return result;
            }
        }
        return false;
    }
    
    @Nullable
    private Boolean findPluginInDirectory(@Nonnull final PluginIdentifier identifier, @Nonnull final Path modsPath) {
        if (!Files.isDirectory(modsPath, new LinkOption[0])) {
            return null;
        }
        try (final DirectoryStream<Path> stream = Files.newDirectoryStream(modsPath)) {
            for (final Path file : stream) {
                if (!Files.isRegularFile(file, new LinkOption[0])) {
                    continue;
                }
                if (!file.getFileName().toString().toLowerCase().endsWith(".jar")) {
                    continue;
                }
                final PluginManifest manifest = loadManifest(file);
                if (manifest == null || !new PluginIdentifier(manifest).equals(identifier)) {
                    continue;
                }
                final PendingLoadJavaPlugin pendingLoadJavaPlugin = this.loadPendingJavaPlugin(file);
                if (pendingLoadJavaPlugin != null) {
                    final Boolean value = this.load(pendingLoadJavaPlugin);
                    if (stream != null) {
                        stream.close();
                    }
                    return value;
                }
                final Boolean value2 = false;
                if (stream != null) {
                    stream.close();
                }
                return value2;
            }
        }
        catch (final IOException e) {
            PluginManager.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to find plugins in %s!", modsPath);
        }
        return null;
    }
    
    @Nullable
    private static PluginManifest loadManifest(@Nonnull final Path file) {
        try {
            final URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { file.toUri().toURL() }, PluginManager.class.getClassLoader());
            try (final InputStream stream = urlClassLoader.findResource("manifest.json").openStream()) {
                try (final InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
                    final char[] buffer = RawJsonReader.READ_BUFFER.get();
                    final RawJsonReader rawJsonReader = new RawJsonReader(reader, buffer);
                    final ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
                    final PluginManifest manifest = PluginManifest.CODEC.decodeJson(rawJsonReader, extraInfo);
                    extraInfo.getValidationResults().logOrThrowValidatorExceptions(PluginManager.LOGGER);
                    return manifest;
                }
                urlClassLoader.close();
            }
            catch (final Throwable t3) {
                try {
                    urlClassLoader.close();
                }
                catch (final Throwable exception3) {
                    t3.addSuppressed(exception3);
                }
                throw t3;
            }
        }
        catch (final IOException e) {
            PluginManager.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to load manifest %s.", file);
            return null;
        }
    }
    
    private boolean load(@Nullable final PendingLoadPlugin pendingLoadPlugin) {
        if (pendingLoadPlugin == null) {
            return false;
        }
        this.validatePluginDeps(pendingLoadPlugin, null);
        final PluginBase plugin = pendingLoadPlugin.load();
        if (plugin != null) {
            this.lock.writeLock().lock();
            try {
                this.plugins.put(plugin.getIdentifier(), plugin);
            }
            finally {
                this.lock.writeLock().unlock();
            }
            final CompletableFuture<Void> preload = plugin.preLoad();
            if (preload == null) {
                final boolean result = this.setup(plugin) && this.start(plugin);
                this.pluginListPageManager.notifyPluginChange(this.plugins, plugin.getIdentifier());
                return result;
            }
            preload.thenAccept(v -> {
                this.setup(plugin);
                this.start(plugin);
                this.pluginListPageManager.notifyPluginChange(this.plugins, plugin.getIdentifier());
                return;
            });
        }
        this.pluginListPageManager.notifyPluginChange(this.plugins, pendingLoadPlugin.getIdentifier());
        return false;
    }
    
    private boolean setup(@Nonnull final PluginBase plugin) {
        if (plugin.getState() == PluginState.NONE && this.dependenciesMatchState(plugin, PluginState.SETUP, PluginState.SETUP)) {
            PluginManager.LOGGER.at(Level.FINE).log("Setting up plugin %s", plugin.getIdentifier());
            final boolean prev = AssetStore.DISABLE_DYNAMIC_DEPENDENCIES;
            AssetStore.DISABLE_DYNAMIC_DEPENDENCIES = false;
            plugin.setup0();
            AssetStore.DISABLE_DYNAMIC_DEPENDENCIES = prev;
            AssetModule.get().initPendingStores();
            HytaleServer.get().doneSetup(plugin);
            if (plugin.getState() != PluginState.DISABLED) {
                final IEventDispatcher<PluginSetupEvent, PluginSetupEvent> dispatch = HytaleServer.get().getEventBus().dispatchFor((Class<? super PluginSetupEvent>)PluginSetupEvent.class, plugin.getClass());
                if (dispatch.hasListener()) {
                    dispatch.dispatch(new PluginSetupEvent(plugin));
                }
                return true;
            }
            plugin.shutdown0(false);
            this.plugins.remove(plugin.getIdentifier());
        }
        else {
            plugin.shutdown0(false);
            this.plugins.remove(plugin.getIdentifier());
        }
        return false;
    }
    
    private boolean start(@Nonnull final PluginBase plugin) {
        if (plugin.getState() == PluginState.SETUP && this.dependenciesMatchState(plugin, PluginState.ENABLED, PluginState.START)) {
            PluginManager.LOGGER.at(Level.FINE).log("Starting plugin %s", plugin.getIdentifier());
            plugin.start0();
            HytaleServer.get().doneStart(plugin);
            if (plugin.getState() != PluginState.DISABLED) {
                PluginManager.LOGGER.at(Level.INFO).log("Enabled plugin %s", plugin.getIdentifier());
                return true;
            }
            plugin.shutdown0(false);
            this.plugins.remove(plugin.getIdentifier());
        }
        else {
            plugin.shutdown0(false);
            this.plugins.remove(plugin.getIdentifier());
        }
        return false;
    }
    
    private boolean dependenciesMatchState(final PluginBase plugin, final PluginState requiredState, final PluginState stage) {
        final Set<PluginIdentifier> dependenciesOnManifest = plugin.getManifest().getDependencies().keySet();
        for (PluginIdentifier dependencyOnManifest : dependenciesOnManifest) {
            final PluginBase dependency = this.plugins.get(dependencyOnManifest);
            if (dependency == null || dependency.getState() != requiredState) {
                PluginManager.LOGGER.at(Level.SEVERE).log(plugin.getName() + " is lacking dependency " + dependencyOnManifest.getName() + " at stage " + String.valueOf(stage));
                PluginManager.LOGGER.at(Level.SEVERE).log(plugin.getName() + " DISABLED!");
                return false;
            }
        }
        return true;
    }
    
    private static void loadPendingPlugin(@Nonnull final Map<PluginIdentifier, PendingLoadPlugin> pending, @Nonnull final PendingLoadPlugin plugin) {
        if (pending.putIfAbsent(plugin.getIdentifier(), plugin) != null) {
            throw new IllegalArgumentException("Tried to load duplicate plugin");
        }
        for (final PendingLoadPlugin subPlugin : plugin.createSubPendingLoadPlugins()) {
            loadPendingPlugin(pending, subPlugin);
        }
    }
    
    @Nonnull
    public Map<PluginIdentifier, PluginManifest> getAvailablePlugins() {
        return this.availablePlugins;
    }
    
    public ComponentType<EntityStore, PluginListPageManager.SessionSettings> getSessionSettingsComponentType() {
        return this.sessionSettingsComponentType;
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
        MODS_PATH = Path.of("mods", new String[0]);
        METRICS_REGISTRY = new MetricsRegistry<PluginManager>().register("Plugins", pluginManager -> pluginManager.getPlugins().toArray(PluginBase[]::new), (Codec<PluginBase[]>)new ArrayCodec<PluginBase>(PluginBase.METRICS_REGISTRY, PluginBase[]::new));
    }
    
    public static class PluginBridgeClassLoader extends ClassLoader
    {
        private final PluginManager pluginManager;
        
        public PluginBridgeClassLoader(final PluginManager pluginManager, final ClassLoader parent) {
            super(parent);
            this.pluginManager = pluginManager;
        }
        
        @Nonnull
        @Override
        protected Class<?> loadClass(@Nonnull final String name, final boolean resolve) throws ClassNotFoundException {
            return this.loadClass0(name, null);
        }
        
        @Nonnull
        public Class<?> loadClass0(@Nonnull final String name, final PluginClassLoader pluginClassLoader) throws ClassNotFoundException {
            this.pluginManager.lock.readLock().lock();
            try {
                for (final Map.Entry<PluginIdentifier, PluginBase> entry : this.pluginManager.plugins.entrySet()) {
                    final PluginBase pluginBase = entry.getValue();
                    final Class<?> loadClass = tryGetClass(name, pluginClassLoader, pluginBase);
                    if (loadClass != null) {
                        return loadClass;
                    }
                }
            }
            finally {
                this.pluginManager.lock.readLock().unlock();
            }
            throw new ClassNotFoundException();
        }
        
        @Nonnull
        public Class<?> loadClass0(@Nonnull final String name, final PluginClassLoader pluginClassLoader, @Nonnull final PluginManifest manifest) throws ClassNotFoundException {
            this.pluginManager.lock.readLock().lock();
            try {
                for (final PluginIdentifier pluginIdentifier : manifest.getDependencies().keySet()) {
                    final PluginBase pluginBase = this.pluginManager.plugins.get(pluginIdentifier);
                    final Class<?> loadClass = tryGetClass(name, pluginClassLoader, pluginBase);
                    if (loadClass != null) {
                        return loadClass;
                    }
                }
                for (final PluginIdentifier pluginIdentifier : manifest.getOptionalDependencies().keySet()) {
                    if (manifest.getDependencies().containsKey(pluginIdentifier)) {
                        continue;
                    }
                    final PluginBase pluginBase = this.pluginManager.plugins.get(pluginIdentifier);
                    if (pluginBase == null) {
                        continue;
                    }
                    final Class<?> loadClass = tryGetClass(name, pluginClassLoader, pluginBase);
                    if (loadClass != null) {
                        return loadClass;
                    }
                }
                for (final Map.Entry<PluginIdentifier, PluginBase> entry : this.pluginManager.plugins.entrySet()) {
                    if (manifest.getDependencies().containsKey(entry.getKey())) {
                        continue;
                    }
                    if (manifest.getOptionalDependencies().containsKey(entry.getKey())) {
                        continue;
                    }
                    final PluginBase pluginBase = entry.getValue();
                    final Class<?> loadClass = tryGetClass(name, pluginClassLoader, pluginBase);
                    if (loadClass != null) {
                        return loadClass;
                    }
                }
            }
            finally {
                this.pluginManager.lock.readLock().unlock();
            }
            throw new ClassNotFoundException();
        }
        
        public static Class<?> tryGetClass(@Nonnull final String name, final PluginClassLoader pluginClassLoader, final PluginBase pluginBase) {
            if (!(pluginBase instanceof JavaPlugin)) {
                return null;
            }
            try {
                final PluginClassLoader classLoader = ((JavaPlugin)pluginBase).getClassLoader();
                if (classLoader != pluginClassLoader) {
                    final Class<?> loadClass = classLoader.loadLocalClass(name);
                    if (loadClass != null) {
                        return loadClass;
                    }
                }
            }
            catch (final ClassNotFoundException ex) {}
            return null;
        }
        
        @Nullable
        public URL getResource0(@Nonnull final String name, @Nullable final PluginClassLoader pluginClassLoader) {
            this.pluginManager.lock.readLock().lock();
            try {
                for (final Map.Entry<PluginIdentifier, PluginBase> entry : this.pluginManager.plugins.entrySet()) {
                    final URL resource = tryGetResource(name, pluginClassLoader, entry.getValue());
                    if (resource != null) {
                        return resource;
                    }
                }
            }
            finally {
                this.pluginManager.lock.readLock().unlock();
            }
            return null;
        }
        
        @Nullable
        public URL getResource0(@Nonnull final String name, @Nullable final PluginClassLoader pluginClassLoader, @Nonnull final PluginManifest manifest) {
            this.pluginManager.lock.readLock().lock();
            try {
                for (final PluginIdentifier pluginIdentifier : manifest.getDependencies().keySet()) {
                    final URL resource = tryGetResource(name, pluginClassLoader, this.pluginManager.plugins.get(pluginIdentifier));
                    if (resource != null) {
                        return resource;
                    }
                }
                for (final PluginIdentifier pluginIdentifier : manifest.getOptionalDependencies().keySet()) {
                    if (manifest.getDependencies().containsKey(pluginIdentifier)) {
                        continue;
                    }
                    final PluginBase pluginBase = this.pluginManager.plugins.get(pluginIdentifier);
                    if (pluginBase == null) {
                        continue;
                    }
                    final URL resource2 = tryGetResource(name, pluginClassLoader, pluginBase);
                    if (resource2 != null) {
                        return resource2;
                    }
                }
                for (final Map.Entry<PluginIdentifier, PluginBase> entry : this.pluginManager.plugins.entrySet()) {
                    if (manifest.getDependencies().containsKey(entry.getKey())) {
                        continue;
                    }
                    if (manifest.getOptionalDependencies().containsKey(entry.getKey())) {
                        continue;
                    }
                    final URL resource = tryGetResource(name, pluginClassLoader, entry.getValue());
                    if (resource != null) {
                        return resource;
                    }
                }
            }
            finally {
                this.pluginManager.lock.readLock().unlock();
            }
            return null;
        }
        
        @Nonnull
        public Enumeration<URL> getResources0(@Nonnull final String name, @Nullable final PluginClassLoader pluginClassLoader) {
            final ObjectArrayList<URL> results = new ObjectArrayList<URL>();
            this.pluginManager.lock.readLock().lock();
            try {
                for (final Map.Entry<PluginIdentifier, PluginBase> entry : this.pluginManager.plugins.entrySet()) {
                    final URL resource = tryGetResource(name, pluginClassLoader, entry.getValue());
                    if (resource != null) {
                        results.add(resource);
                    }
                }
            }
            finally {
                this.pluginManager.lock.readLock().unlock();
            }
            return Collections.enumeration(results);
        }
        
        @Nonnull
        public Enumeration<URL> getResources0(@Nonnull final String name, @Nullable final PluginClassLoader pluginClassLoader, @Nonnull final PluginManifest manifest) {
            final ObjectArrayList<URL> results = new ObjectArrayList<URL>();
            this.pluginManager.lock.readLock().lock();
            try {
                for (final PluginIdentifier pluginIdentifier : manifest.getDependencies().keySet()) {
                    final URL resource = tryGetResource(name, pluginClassLoader, this.pluginManager.plugins.get(pluginIdentifier));
                    if (resource != null) {
                        results.add(resource);
                    }
                }
                for (final PluginIdentifier pluginIdentifier : manifest.getOptionalDependencies().keySet()) {
                    if (manifest.getDependencies().containsKey(pluginIdentifier)) {
                        continue;
                    }
                    final PluginBase pluginBase = this.pluginManager.plugins.get(pluginIdentifier);
                    if (pluginBase == null) {
                        continue;
                    }
                    final URL resource2 = tryGetResource(name, pluginClassLoader, pluginBase);
                    if (resource2 == null) {
                        continue;
                    }
                    results.add(resource2);
                }
                for (final Map.Entry<PluginIdentifier, PluginBase> entry : this.pluginManager.plugins.entrySet()) {
                    if (manifest.getDependencies().containsKey(entry.getKey())) {
                        continue;
                    }
                    if (manifest.getOptionalDependencies().containsKey(entry.getKey())) {
                        continue;
                    }
                    final URL resource = tryGetResource(name, pluginClassLoader, entry.getValue());
                    if (resource == null) {
                        continue;
                    }
                    results.add(resource);
                }
            }
            finally {
                this.pluginManager.lock.readLock().unlock();
            }
            return Collections.enumeration(results);
        }
        
        @Nullable
        private static URL tryGetResource(@Nonnull final String name, @Nullable final PluginClassLoader pluginClassLoader, @Nullable final PluginBase pluginBase) {
            if (!(pluginBase instanceof JavaPlugin)) {
                return null;
            }
            final JavaPlugin javaPlugin = (JavaPlugin)pluginBase;
            final PluginClassLoader classLoader = javaPlugin.getClassLoader();
            if (classLoader != pluginClassLoader) {
                return classLoader.findResource(name);
            }
            return null;
        }
        
        static {
            registerAsParallelCapable();
        }
    }
}
