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

package com.hypixel.hytale.server.npc.asset.builder;

import com.google.gson.JsonSerializationContext;
import com.hypixel.hytale.server.core.asset.monitor.EventKind;
import com.hypixel.hytale.server.core.util.NotificationUtil;
import com.hypixel.hytale.protocol.packets.interface_.NotificationStyle;
import com.hypixel.hytale.server.npc.validators.NPCLoadTimeValidationHelper;
import com.hypixel.hytale.server.core.asset.type.model.config.Model;
import com.hypixel.hytale.server.core.asset.type.model.config.ModelAsset;
import com.hypixel.hytale.server.npc.util.expression.Scope;
import com.hypixel.hytale.server.npc.util.expression.ExecutionContext;
import com.hypixel.hytale.server.spawning.LoadedNPCEvent;
import com.hypixel.hytale.event.IEventDispatcher;
import com.hypixel.hytale.server.npc.AllNPCsLoadedEvent;
import com.hypixel.hytale.server.core.HytaleServer;
import com.google.gson.Gson;
import java.io.BufferedWriter;
import com.hypixel.hytale.server.npc.asset.builder.providerevaluators.ProviderEvaluatorTypeRegistry;
import com.hypixel.hytale.server.npc.asset.builder.validators.ValidatorTypeRegistry;
import java.time.Period;
import java.lang.reflect.Type;
import com.google.gson.JsonPrimitive;
import java.time.Duration;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.GsonBuilder;
import java.nio.file.OpenOption;
import java.util.Objects;
import com.hypixel.hytale.common.util.ArrayUtil;
import com.hypixel.hytale.codec.schema.config.StringSchema;
import com.hypixel.hytale.codec.schema.config.ObjectSchema;
import com.hypixel.hytale.server.npc.role.Role;
import com.hypixel.hytale.codec.schema.config.Schema;
import com.hypixel.hytale.codec.schema.SchemaContext;
import it.unimi.dsi.fastutil.ints.IntIterator;
import java.util.function.BiPredicate;
import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.BufferedReader;
import com.hypixel.hytale.codec.ExtraInfo;
import com.hypixel.hytale.server.npc.decisionmaker.core.Evaluator;
import com.hypixel.hytale.assetstore.JsonAsset;
import com.hypixel.hytale.assetstore.AssetExtraInfo;
import com.hypixel.hytale.server.npc.util.expression.StdLib;
import com.google.gson.JsonParser;
import java.io.Reader;
import com.google.gson.stream.JsonReader;
import java.util.Set;
import com.hypixel.hytale.server.core.Message;
import java.util.HashSet;
import it.unimi.dsi.fastutil.objects.ObjectListIterator;
import com.hypixel.hytale.logger.sentry.SkipSentryException;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.List;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.server.core.asset.monitor.AssetMonitorHandler;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import com.hypixel.hytale.server.core.asset.monitor.AssetMonitor;
import com.hypixel.hytale.sneakythrow.SneakyThrow;
import java.nio.file.FileVisitor;
import java.io.IOException;
import java.util.logging.Level;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import com.hypixel.hytale.server.core.util.io.FileUtil;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import com.hypixel.hytale.server.core.asset.AssetModule;
import com.hypixel.hytale.assetstore.AssetPack;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import it.unimi.dsi.fastutil.objects.Object2IntMaps;
import java.util.Iterator;
import it.unimi.dsi.fastutil.Hash;
import it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap;
import com.hypixel.hytale.assetstore.map.CaseInsensitiveHashStrategy;
import java.util.HashMap;
import com.hypixel.hytale.server.npc.NPCPlugin;
import javax.annotation.Nullable;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import java.util.Map;
import com.hypixel.fastutil.ints.Int2ObjectConcurrentHashMap;

public class BuilderManager
{
    public static final String CONTENT_KEY = "Content";
    private static final String CLASS_KEY = "Class";
    private static final String TEST_TYPE_KEY = "TestType";
    private static final String FAIL_REASON_KEY = "FailReason";
    private static final String PLAYER_GROUP_TAG = "$player";
    private static final String SELF_GROUP_TAG = "$self";
    private static int playerGroupID;
    private static int selfGroupID;
    private final Int2ObjectConcurrentHashMap<BuilderInfo> builderCache;
    private final String elementTypeName = "NPC";
    private final String defaultFileType;
    private boolean autoReload;
    private final Map<Class<?>, BuilderFactory<?>> factoryMap;
    private final Map<String, Class<?>> categoryNames;
    @Nonnull
    private final Object2IntMap<String> nameToIndexMap;
    private final AtomicInteger nextIndex;
    private final ReentrantReadWriteLock indexLock;
    private boolean setup;
    @Nullable
    public static BuilderManager SCHEMA_BUILDER_MANAGER;
    
    public BuilderManager() {
        this.builderCache = new Int2ObjectConcurrentHashMap<BuilderInfo>();
        this.defaultFileType = NPCPlugin.FACTORY_CLASS_ROLE;
        this.factoryMap = new HashMap<Class<?>, BuilderFactory<?>>();
        this.categoryNames = new HashMap<String, Class<?>>();
        this.nextIndex = new AtomicInteger();
        this.indexLock = new ReentrantReadWriteLock();
        (this.nameToIndexMap = new Object2IntOpenCustomHashMap<String>(CaseInsensitiveHashStrategy.getInstance())).defaultReturnValue(Integer.MIN_VALUE);
        BuilderManager.playerGroupID = this.getOrCreateIndex("$player");
        BuilderManager.selfGroupID = this.getOrCreateIndex("$self");
    }
    
    public <T> void registerFactory(@Nonnull final BuilderFactory<T> factory) {
        if (factory == null) {
            throw new IllegalArgumentException();
        }
        final Class<?> clazz = factory.getCategory();
        if (clazz == null) {
            throw new IllegalArgumentException();
        }
        if (this.factoryMap.containsKey(clazz)) {
            throw new IllegalArgumentException(factory.getClass().getSimpleName());
        }
        this.factoryMap.put(clazz, factory);
    }
    
    public void addCategory(final String name, final Class<?> clazz) {
        this.categoryNames.put(name, clazz);
    }
    
    public String getCategoryName(@Nonnull final Class<?> factoryClass) {
        for (final Map.Entry<String, Class<?>> stringClassEntry : this.categoryNames.entrySet()) {
            if (stringClassEntry.getValue() == factoryClass) {
                return stringClassEntry.getKey();
            }
        }
        return factoryClass.getSimpleName();
    }
    
    public int getIndex(@Nullable final String name) {
        if (name == null || name.isEmpty()) {
            return Integer.MIN_VALUE;
        }
        this.indexLock.readLock().lock();
        try {
            return this.nameToIndexMap.getInt(name);
        }
        finally {
            this.indexLock.readLock().unlock();
        }
    }
    
    public void setAutoReload(final boolean autoReload) {
        this.autoReload = autoReload;
    }
    
    @Nullable
    public String lookupName(final int index) {
        if (index < 0) {
            return null;
        }
        final BuilderInfo info = this.builderCache.get(index);
        if (info != null) {
            return info.getKeyName();
        }
        this.indexLock.readLock().lock();
        try {
            final ObjectIterator<Object2IntMap.Entry<String>> iterator = Object2IntMaps.fastIterator(this.nameToIndexMap);
            while (iterator.hasNext()) {
                final Object2IntMap.Entry<String> entry = iterator.next();
                if (entry.getIntValue() == index) {
                    return entry.getKey();
                }
            }
        }
        finally {
            this.indexLock.readLock().unlock();
        }
        return null;
    }
    
    public int getOrCreateIndex(final String name) {
        this.indexLock.writeLock().lock();
        try {
            int index = this.nameToIndexMap.getInt(name);
            if (index >= 0) {
                return index;
            }
            index = this.nextIndex.getAndIncrement();
            this.nameToIndexMap.put(name, index);
            return index;
        }
        finally {
            this.indexLock.writeLock().unlock();
        }
    }
    
    @Nullable
    public BuilderInfo tryGetBuilderInfo(final int builderIndex) {
        return (builderIndex < 0) ? null : this.builderCache.get(builderIndex);
    }
    
    public void unloadBuilders(final AssetPack pack) {
        final Path path = pack.getRoot().resolve(NPCPlugin.ROLE_ASSETS_PATH);
        final AssetMonitor assetMonitor = AssetModule.get().getAssetMonitor();
        if (assetMonitor != null) {
            assetMonitor.removeMonitorDirectoryFiles(path, pack);
        }
        if (!Files.isDirectory(path, new LinkOption[0])) {
            return;
        }
        try {
            Files.walkFileTree(path, FileUtil.DEFAULT_WALK_TREE_OPTIONS_SET, Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
                @Nonnull
                @Override
                public FileVisitResult visitFile(@Nonnull final Path file, @Nonnull final BasicFileAttributes attrs) {
                    if (BuilderManager.isJsonFile(file) && !BuilderManager.isIgnoredFile(file)) {
                        final String builderName = BuilderManager.builderNameFromPath(file);
                        BuilderManager.this.removeBuilder(builderName);
                        NPCPlugin.get().getLogger().at(Level.INFO).log("Deleted %s builder %s", "NPC", builderName);
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        }
        catch (final IOException e) {
            throw SneakyThrow.sneakyThrow(e);
        }
    }
    
    public boolean loadBuilders(@Nonnull final AssetPack pack, final boolean includeTests) {
        final Path path = pack.getRoot().resolve(NPCPlugin.ROLE_ASSETS_PATH);
        boolean valid = true;
        NPCPlugin.get().getLogger().at(Level.INFO).log("Starting to load NPC builders!");
        final Object2IntOpenHashMap<String> typeCounter = new Object2IntOpenHashMap<String>();
        try {
            final AssetMonitor assetMonitor = AssetModule.get().getAssetMonitor();
            if (assetMonitor != null && !pack.isImmutable() && Files.isDirectory(path, new LinkOption[0])) {
                assetMonitor.removeMonitorDirectoryFiles(path, pack);
                assetMonitor.monitorDirectoryFiles(path, new BuilderAssetMonitorHandler(pack, includeTests));
            }
            final ObjectArrayList<String> errors = new ObjectArrayList<String>();
            if (Files.isDirectory(path, new LinkOption[0])) {
                Files.walkFileTree(path, FileUtil.DEFAULT_WALK_TREE_OPTIONS_SET, Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
                    @Nonnull
                    @Override
                    public FileVisitResult visitFile(@Nonnull final Path file, @Nonnull final BasicFileAttributes attrs) {
                        if (BuilderManager.isJsonFile(file) && !BuilderManager.isIgnoredFile(file)) {
                            BuilderManager.this.loadFile(file, errors, typeCounter, includeTests, false);
                        }
                        return FileVisitResult.CONTINUE;
                    }
                });
            }
            final Int2ObjectOpenHashMap<BuilderInfo> loadedBuilders = new Int2ObjectOpenHashMap<BuilderInfo>();
            for (final BuilderInfo builderInfo : this.builderCache.values()) {
                try {
                    if (this.validateBuilder(builderInfo)) {
                        loadedBuilders.put(builderInfo.getIndex(), builderInfo);
                    }
                    else {
                        valid = false;
                    }
                }
                catch (final IllegalArgumentException | IllegalStateException e) {
                    valid = false;
                    errors.add(String.format("%s: %s", builderInfo.getKeyName(), e.getMessage()));
                }
            }
            this.setup = true;
            this.validateAllLoadedBuilders(loadedBuilders, false, errors);
            if (!errors.isEmpty()) {
                valid = false;
                for (String error : errors) {
                    NPCPlugin.get().getLogger().at(Level.SEVERE).log("FAIL: " + error);
                }
            }
            errors.clear();
            this.onAllBuildersLoaded(loadedBuilders);
        }
        catch (final IOException e2) {
            throw new SkipSentryException(new RuntimeException(e2));
        }
        final StringBuilder output = new StringBuilder();
        output.append("Loaded ").append(this.builderCache.size()).append(" ").append("NPC").append(" configurations");
        for (final Object2IntMap.Entry<String> entry : typeCounter.object2IntEntrySet()) {
            output.append(", ").append(entry.getKey()).append(": ").append(entry.getIntValue());
        }
        NPCPlugin.get().getLogger().at(Level.INFO).log(output.toString());
        return valid;
    }
    
    private void finishLoadingBuilders(@Nonnull final Int2ObjectOpenHashMap<BuilderInfo> loadedBuilders, @Nonnull final List<String> errors) {
        this.onAllBuildersLoaded(loadedBuilders);
        this.validateAllLoadedBuilders(loadedBuilders, true, errors);
        if (!errors.isEmpty()) {
            for (final String error : errors) {
                NPCPlugin.get().getLogger().at(Level.SEVERE).log(error);
            }
        }
        errors.clear();
    }
    
    public void assetEditorLoadFile(@Nonnull final Path fileName) {
        final HashSet<String> failedBuilderTexts = new HashSet<String>();
        final ObjectArrayList<String> errors = new ObjectArrayList<String>();
        final Int2ObjectOpenHashMap<BuilderInfo> loadedBuilders = new Int2ObjectOpenHashMap<BuilderInfo>();
        final HashSet<String> loadedBuilderNames = new HashSet<String>();
        try {
            final int builderIndex = this.loadFile(fileName, errors, null, true, true);
            if (builderIndex < 0) {
                return;
            }
            final String name = builderNameFromPath(fileName);
            NPCPlugin.get().getLogger().at(Level.INFO).log("Reloaded NPC builder " + name);
            loadedBuilderNames.add(name);
            for (final BuilderInfo builderInfo : this.builderCache.values()) {
                if (this.isDependant(builderInfo.getBuilder(), builderInfo.getIndex(), builderIndex)) {
                    builderInfo.setNeedsValidation();
                }
            }
            if (this.autoReload) {
                this.reloadDependants(builderIndex);
            }
            final BuilderInfo builder = this.builderCache.get(builderIndex);
            onBuilderReloaded(builder);
            loadedBuilders.put(builderIndex, builder);
        }
        catch (final Throwable e) {
            NPCPlugin.get().getLogger().at(Level.SEVERE).log("Failed to reload %s config %s: %s", "NPC", fileName, e.getMessage());
            failedBuilderTexts.add(builderNameFromPath(fileName) + ": " + e.getMessage());
        }
        sendReloadNotification(Message.translation("server.general.assetstore.reloadAssets").param("class", "NPC"), loadedBuilderNames);
        sendReloadNotification(Message.translation("server.general.assetstore.loadFailed").param("class", "NPC"), failedBuilderTexts);
        this.finishLoadingBuilders(loadedBuilders, errors);
    }
    
    public void assetEditorRemoveFile(@Nonnull final Path filePath) {
        final String builderName = builderNameFromPath(filePath);
        this.removeBuilder(builderName);
        NPCPlugin.get().getLogger().at(Level.INFO).log("Deleted %s builder %s", "NPC", builderName);
        sendReloadNotification(Message.translation("server.general.assetstore.removedAssets").param("class", "NPC"), Set.of(builderName));
        final ObjectArrayList<String> errors = new ObjectArrayList<String>();
        this.finishLoadingBuilders(new Int2ObjectOpenHashMap<BuilderInfo>(), errors);
    }
    
    public int loadFile(@Nonnull final Path fileName, final boolean reloading, @Nonnull final List<String> errors) {
        return this.loadFile(fileName, errors, null, false, reloading);
    }
    
    public int loadFile(@Nonnull final Path fileName, @Nonnull final List<String> errors, @Nullable final Object2IntMap<String> typeCounter, final boolean includeTests, final boolean reloading) {
        final int errorCount = errors.size();
        JsonObject data;
        try (final BufferedReader fileReader = Files.newBufferedReader(fileName);
             final JsonReader reader = new JsonReader(fileReader)) {
            data = JsonParser.parseReader(reader).getAsJsonObject();
        }
        catch (final Exception e) {
            errors.add(String.valueOf(fileName) + ": Failed to load NPC builder: " + e.getMessage());
            return Integer.MIN_VALUE;
        }
        String categoryName = this.defaultFileType;
        final JsonElement content = data;
        TestType testType = null;
        final JsonElement testTypeElement = data.get("TestType");
        if (testTypeElement != null) {
            try {
                testType = Enum.valueOf(TestType.class, testTypeElement.getAsString().toUpperCase());
            }
            catch (final Exception e2) {
                errors.add(String.valueOf(fileName) + ": " + e2.getMessage());
            }
            if (!includeTests) {
                return Integer.MIN_VALUE;
            }
        }
        final String keyName = builderNameFromPath(fileName);
        String componentInterface = null;
        final StateMappingHelper stateHelper = new StateMappingHelper();
        final JsonElement classData = data.get("Class");
        if (classData != null) {
            categoryName = classData.getAsString();
            stateHelper.readComponentDefaultLocalState(data);
            final JsonElement interfaceData = data.get("Interface");
            if (interfaceData != null) {
                componentInterface = interfaceData.getAsString();
            }
        }
        final Class<?> category = this.categoryNames.get(categoryName);
        if (category == null) {
            errors.add(String.valueOf(fileName) + ": Failed to load NPC builder, unknown class " + categoryName);
            return Integer.MIN_VALUE;
        }
        if (typeCounter != null) {
            final JsonElement type = data.get("Type");
            final String typeString = (testType == null) ? ((type != null) ? type.getAsString() : categoryName) : "Test";
            typeCounter.mergeInt(typeString, 1, Integer::sum);
        }
        final BuilderFactory<Object> factory = this.getFactory(category);
        Builder<?> builder;
        try {
            builder = factory.createBuilder(content);
        }
        catch (final Exception e3) {
            errors.add(String.valueOf(fileName) + ": " + e3.getMessage());
            return Integer.MIN_VALUE;
        }
        final String fileNameString = fileName.toString();
        this.checkIfDeprecated(builder, factory, content, fileNameString, categoryName);
        builder.setLabel(categoryName + "|" + factory.getKeyName(content));
        builder.ignoreAttribute("TestType");
        if (testType == TestType.FAILING) {
            builder.ignoreAttribute("FailReason");
        }
        builder.ignoreAttribute("Parameters");
        final BuilderParameters builderParameters = new BuilderParameters(StdLib.getInstance(), fileNameString, componentInterface);
        try {
            builderParameters.readJSON(data, stateHelper);
        }
        catch (final Exception e4) {
            errors.add(fileNameString + ": Failed to load NPC builder, 'Parameters' section invalid: " + e4.getMessage());
            return Integer.MIN_VALUE;
        }
        if (classData != null) {
            builder.ignoreAttribute("Class");
            builder.ignoreAttribute("Interface");
            builder.ignoreAttribute("DefaultState");
            builder.ignoreAttribute("ResetOnStateChange");
        }
        builderParameters.addParametersToScope();
        final InternalReferenceResolver internalReferenceResolver = new InternalReferenceResolver();
        final AssetExtraInfo.Data extraInfoData = new AssetExtraInfo.Data(null, (K)keyName, (K)null);
        final AssetExtraInfo<Object> extraInfo = new AssetExtraInfo<Object>(extraInfoData);
        final ObjectArrayList<Evaluator<?>> evaluators = new ObjectArrayList<Evaluator<?>>();
        final BuilderValidationHelper validationHelper = new BuilderValidationHelper(fileNameString, new FeatureEvaluatorHelper(builder.canRequireFeature()), internalReferenceResolver, stateHelper, new InstructionContextHelper(InstructionType.Component), extraInfo, evaluators, errors);
        try {
            builder.readConfig(null, content, this, builderParameters, validationHelper);
        }
        catch (final Exception e5) {
            errors.add(fileNameString + ": Failed to load NPC: " + e5.getMessage());
            return Integer.MIN_VALUE;
        }
        internalReferenceResolver.validateInternalReferences(fileNameString, errors);
        extraInfoData.loadContainedAssets(reloading);
        for (final Evaluator<?> evaluator : evaluators) {
            evaluator.initialise();
        }
        internalReferenceResolver.optimise();
        builderParameters.disposeCompileContext();
        stateHelper.validate(fileNameString, errors);
        stateHelper.optimise();
        final BuilderInfo entry = this.tryGetBuilderInfo(this.getIndex(keyName));
        if (entry != null && entry.getPath() != null) {
            try {
                if (!Files.isSameFile(fileName, entry.getPath())) {
                    NPCPlugin.get().getLogger().at(Level.WARNING).log("Replacing asset '%s' of file '%s' with other file '%s'", keyName, entry.getPath(), fileName);
                }
            }
            catch (final IOException ex) {}
        }
        if (testType != TestType.FAILING) {
            return (errors.size() > errorCount) ? Integer.MIN_VALUE : this.cacheBuilder(keyName, builder, fileName);
        }
        final JsonElement failReasonElement = data.get("FailReason");
        if (failReasonElement == null) {
            errors.add(String.valueOf(fileName) + ": Missing fail reason!");
            return Integer.MIN_VALUE;
        }
        if (errors.size() == errorCount) {
            errors.add(String.valueOf(fileName) + ": Should have failed validation: " + failReasonElement.getAsString());
            return Integer.MIN_VALUE;
        }
        if (errors.size() - errorCount > 1) {
            errors.add(String.valueOf(fileName) + ": Should have failed validation: " + failReasonElement.getAsString() + ", but additional errors were also detected.");
            return Integer.MIN_VALUE;
        }
        final String error = errors.removeLast();
        if (!error.contains(failReasonElement.getAsString())) {
            errors.add(String.valueOf(fileName) + ": Should have failed validation: " + failReasonElement.getAsString() + ", but was instead: " + error);
            return Integer.MIN_VALUE;
        }
        if (NPCPlugin.get().isLogFailingTestErrors()) {
            NPCPlugin.get().getLogger().at(Level.WARNING).log("Expected test failure: " + error);
        }
        return Integer.MIN_VALUE;
    }
    
    public boolean validateBuilder(@Nonnull final BuilderInfo builderInfo) {
        if (builderInfo.isValidated()) {
            return builderInfo.isValid();
        }
        if (!builderInfo.canBeValidated()) {
            return false;
        }
        final Builder<?> builder = builderInfo.getBuilder();
        if (builder.getDependencies().isEmpty() && !builder.hasDynamicDependencies()) {
            return builderInfo.setValidated(true);
        }
        return this.validateBuilder(builderInfo, new IntOpenHashSet(), new IntArrayList());
    }
    
    @Nonnull
    public <T> BuilderFactory<T> getFactory(@Nonnull final Class<?> clazz) {
        if (clazz == null) {
            throw new IllegalArgumentException("No factory class supplied!");
        }
        final BuilderFactory<T> factory = (BuilderFactory<T>)this.factoryMap.get(clazz);
        if (factory == null) {
            throw new NullPointerException(String.format("Factory for type '%s' is not registered!", clazz.getSimpleName()));
        }
        if (factory.getCategory() != clazz) {
            throw new IllegalArgumentException(String.format("Factory class mismatch! Expected %s, was %s", clazz.getSimpleName(), factory.getCategory().getSimpleName()));
        }
        return factory;
    }
    
    @Nonnull
    public BuilderInfo getCachedBuilderInfo(final int index, @Nonnull final Class<?> classType) {
        if (index < 0) {
            throw new SkipSentryException(new IllegalArgumentException("Builder asset can't have negative index " + index));
        }
        final BuilderInfo builderInfo = this.tryGetCachedBuilderInfo(index, classType);
        if (builderInfo == null) {
            throw new SkipSentryException(new IllegalArgumentException(String.format("Asset '%s' (%s) is not available", this.lookupName(index), index)));
        }
        return builderInfo;
    }
    
    @Nullable
    public <T> Builder<T> tryGetCachedValidBuilder(final int index, @Nonnull final Class<?> classType) {
        final BuilderInfo builderInfo = this.tryGetCachedBuilderInfo(index, classType);
        return (Builder<T>)((builderInfo != null && builderInfo.isValid()) ? builderInfo.getBuilder() : null);
    }
    
    public <T> Builder<T> getCachedBuilder(final int index, @Nonnull final Class<?> classType) {
        final BuilderInfo builderInfo = this.getCachedBuilderInfo(index, classType);
        return (Builder<T>)builderInfo.getBuilder();
    }
    
    public boolean isEmpty() {
        return this.builderCache.isEmpty();
    }
    
    @Nonnull
    public Int2ObjectMap<BuilderInfo> getAllBuilders() {
        final Int2ObjectOpenHashMap<BuilderInfo> builders = new Int2ObjectOpenHashMap<BuilderInfo>();
        for (final BuilderInfo builder : this.builderCache.values()) {
            builders.put(builder.getIndex(), builder);
        }
        return builders;
    }
    
    public <T extends Collection<?>> T collectMatchingBuilders(final T collection, @Nonnull final Predicate<BuilderInfo> filter, @Nonnull final BiConsumer<BuilderInfo, T> consumer) {
        for (final BuilderInfo builderInfo : this.builderCache.values()) {
            if (filter.test(builderInfo)) {
                consumer.accept(builderInfo, collection);
            }
        }
        return collection;
    }
    
    @Nonnull
    public Object2IntMap<String> getNameToIndexMap() {
        final Object2IntOpenHashMap<String> map = new Object2IntOpenHashMap<String>();
        if (!this.setup) {
            return Object2IntMaps.unmodifiable((Object2IntMap<? extends String>)map);
        }
        this.indexLock.readLock().lock();
        try {
            final ObjectIterator<Object2IntMap.Entry<String>> iterator = Object2IntMaps.fastIterator(this.nameToIndexMap);
            while (iterator.hasNext()) {
                final Object2IntMap.Entry<String> next = iterator.next();
                map.put(next.getKey(), next.getIntValue());
            }
        }
        finally {
            this.indexLock.readLock().unlock();
        }
        return Object2IntMaps.unmodifiable((Object2IntMap<? extends String>)map);
    }
    
    @Nullable
    public <T> BuilderInfo findMatchingBuilder(@Nonnull final BiPredicate<BuilderInfo, T> filter, final T t) {
        for (final BuilderInfo builderInfo : this.builderCache.values()) {
            if (filter.test(builderInfo, t)) {
                return builderInfo;
            }
        }
        return null;
    }
    
    @Nullable
    public BuilderInfo getBuilderInfo(final Builder<?> builder) {
        return this.findMatchingBuilder((builderInfo, b) -> builderInfo.getBuilder() == b, builder);
    }
    
    public List<String> getTemplateNames() {
        return this.collectMatchingBuilders((ObjectArrayList)new ObjectArrayList(), builderInfo -> true, (builderInfo, strings) -> strings.add(builderInfo.getKeyName()));
    }
    
    public void forceValidation(final int builderIndex) {
        BuilderInfo builderInfo = this.tryGetBuilderInfo(builderIndex);
        if (builderInfo == null) {
            return;
        }
        final IntSet dependencies = this.computeAllDependencies(builderInfo.getBuilder(), builderInfo.getIndex());
        builderInfo.setForceValidation();
        final IntIterator i = dependencies.iterator();
        while (i.hasNext()) {
            builderInfo = this.tryGetBuilderInfo(i.nextInt());
            if (builderInfo != null) {
                builderInfo.setForceValidation();
            }
        }
    }
    
    public void checkIfDeprecated(@Nonnull final Builder<?> builder, @Nonnull final BuilderFactory<?> builderFactory, @Nonnull final JsonElement element, final String fileName, final String context) {
        if (!builder.isDeprecated()) {
            return;
        }
        NPCPlugin.get().getLogger().at(Level.WARNING).log("Builder %s of type %s is deprecated and should be replaced in %s: %s", builderFactory.getKeyName(element), this.getCategoryName(builderFactory.getCategory()), context, fileName);
    }
    
    @Nonnull
    public Schema generateSchema(@Nonnull final SchemaContext context) {
        try {
            BuilderManager.SCHEMA_BUILDER_MANAGER = this;
            final BuilderFactory<?> roleFactory = this.factoryMap.get(Role.class);
            final Schema schema = roleFactory.toSchema(context, true);
            final ObjectSchema check = new ObjectSchema();
            check.setRequired("Class", "Type");
            final StringSchema keys = new StringSchema();
            keys.setEnum(this.categoryNames.keySet().toArray(String[]::new));
            check.setProperties((Map<String, Schema>)Map.of("Class", keys));
            check.setProperties(Map.of("Type", StringSchema.constant("Component")));
            final Schema dynamicComponent = new Schema();
            dynamicComponent.setIf(check);
            final Schema[] subSchemas = new Schema[this.categoryNames.size()];
            int index = 0;
            for (final Map.Entry<String, Class<?>> cats : this.categoryNames.entrySet()) {
                final BuilderFactory<Object> factory = this.getFactory(cats.getValue());
                final Schema s = factory.toSchema(context, true);
                final Schema cond = new Schema();
                final ObjectSchema classCheck = new ObjectSchema();
                classCheck.setProperties(Map.of("Class", StringSchema.constant(cats.getKey())));
                cond.setIf(classCheck);
                cond.setThen(s);
                cond.setElse(false);
                subSchemas[index++] = cond;
            }
            dynamicComponent.setThen(Schema.anyOf(subSchemas));
            dynamicComponent.setElse(false);
            schema.getThen().setAnyOf((Schema[])ArrayUtil.append(schema.getThen().getAnyOf(), dynamicComponent));
            return schema;
        }
        finally {
            BuilderManager.SCHEMA_BUILDER_MANAGER = null;
        }
    }
    
    @Nonnull
    public List<BuilderDescriptor> generateDescriptors() {
        final ObjectArrayList<BuilderDescriptor> builderDescriptors = new ObjectArrayList<BuilderDescriptor>();
        for (final BuilderFactory<?> builderFactory : this.factoryMap.values()) {
            final String categoryName = this.getCategoryName(builderFactory.getCategory());
            final Builder<?> defaultBuilder = builderFactory.tryCreateDefaultBuilder();
            if (defaultBuilder != null) {
                try {
                    builderDescriptors.add(defaultBuilder.getDescriptor(categoryName, categoryName, this));
                }
                catch (final IllegalStateException | NullPointerException e) {
                    NPCPlugin.get().getLogger().at(Level.SEVERE).log("Failed to build descriptor for %s %s: %s", categoryName, categoryName, e.getMessage());
                }
            }
            for (final String builderName : builderFactory.getBuilderNames()) {
                final Builder<?> builder = builderFactory.createBuilder(builderName);
                Objects.requireNonNull(builder, "Unable to create builder for descriptor generation");
                final String name = (builderName == null || builderName.isEmpty()) ? categoryName : builderName;
                if (name.equals("Component")) {
                    continue;
                }
                try {
                    builderDescriptors.add(builder.getDescriptor(name, categoryName, this));
                }
                catch (final IllegalStateException | NullPointerException e2) {
                    NPCPlugin.get().getLogger().at(Level.SEVERE).log("Failed to build descriptor for %s %s: %s", categoryName, name, e2.getMessage());
                }
            }
        }
        return builderDescriptors;
    }
    
    public static void saveDescriptors(final List<BuilderDescriptor> builderDescriptors, @Nonnull final Path fileName) {
        try (final BufferedWriter fileWriter = Files.newBufferedWriter(fileName, new OpenOption[0])) {
            final GsonBuilder gsonBuilder = new GsonBuilder().setPrettyPrinting().serializeNulls().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE);
            gsonBuilder.registerTypeAdapter(Duration.class, (src, typeOfSrc, context) -> new JsonPrimitive(src.toString()));
            gsonBuilder.registerTypeAdapter(Period.class, (src, typeOfSrc, context) -> new JsonPrimitive(src.toString()));
            ValidatorTypeRegistry.registerTypes(gsonBuilder);
            ProviderEvaluatorTypeRegistry.registerTypes(gsonBuilder);
            final Gson gson = gsonBuilder.create();
            gson.toJson(builderDescriptors, fileWriter);
        }
        catch (final IOException e) {
            NPCPlugin.get().getLogger().at(Level.SEVERE).log("Failed to write builder descriptors to %s", fileName);
        }
    }
    
    @Nullable
    public Builder<Role> tryGetCachedValidRole(final int builderIndex) {
        return this.tryGetCachedValidBuilder(builderIndex, Role.class);
    }
    
    public void validateAllLoadedBuilders(@Nonnull final Int2ObjectMap<BuilderInfo> loadedBuilders, final boolean validateDependents, @Nonnull final List<String> errors) {
        NPCPlugin.get().getLogger().at(Level.INFO).log("Validating loaded NPC configurations...");
        validateAllSpawnableNPCs(loadedBuilders, errors);
        if (validateDependents) {
            final Int2ObjectOpenHashMap<BuilderInfo> dependents = new Int2ObjectOpenHashMap<BuilderInfo>();
            loadedBuilders.forEach((index, builderInfo) -> {
                for (final BuilderInfo info : this.builderCache.values()) {
                    final int builderIndex = info.getIndex();
                    final Builder<?> builder = info.getBuilder();
                    boolean isDependent;
                    try {
                        isDependent = this.isDependant(builder, builderIndex, index);
                    }
                    catch (final SkipSentryException | IllegalStateException | IllegalArgumentException e) {
                        NPCPlugin.get().getLogger().at(Level.WARNING).log("Could not check if builder %s was dependent: %s", this.lookupName(info.getIndex()), e.getMessage());
                        continue;
                    }
                    if (builder.isSpawnable() && isDependent) {
                        dependents.put(builderIndex, info);
                    }
                }
                return;
            });
            validateAllSpawnableNPCs(dependents, errors);
        }
        NPCPlugin.get().getLogger().at(Level.INFO).log("Validation complete.");
    }
    
    public void onAllBuildersLoaded(@Nonnull final Int2ObjectMap<BuilderInfo> loadedBuilders) {
        if (!loadedBuilders.isEmpty()) {
            final IEventDispatcher<AllNPCsLoadedEvent, AllNPCsLoadedEvent> dispatcher = HytaleServer.get().getEventBus().dispatchFor((Class<? super AllNPCsLoadedEvent>)AllNPCsLoadedEvent.class);
            if (dispatcher.hasListener()) {
                dispatcher.dispatch(new AllNPCsLoadedEvent(this.getAllBuilders(), loadedBuilders));
            }
            this.getAllBuilders().forEach((index, builderInfo) -> {
                if (builderInfo.needsValidation()) {
                    NPCPlugin.get().testAndValidateRole(builderInfo);
                }
            });
        }
    }
    
    public static void onBuilderReloaded(@Nonnull final BuilderInfo builderInfo) {
        builderInfo.getBuilder().clearDynamicDependencies();
        NPCPlugin.reloadNPCsWithRole(builderInfo.getIndex());
    }
    
    public static int getPlayerGroupID() {
        return BuilderManager.playerGroupID;
    }
    
    public static int getSelfGroupID() {
        return BuilderManager.selfGroupID;
    }
    
    protected static void onBuilderAdded(@Nonnull final BuilderInfo builderInfo) {
        if (builderInfo.getBuilder().isSpawnable()) {
            final IEventDispatcher<LoadedNPCEvent, LoadedNPCEvent> dispatcher = HytaleServer.get().getEventBus().dispatchFor((Class<? super LoadedNPCEvent>)LoadedNPCEvent.class);
            if (dispatcher.hasListener()) {
                dispatcher.dispatch(new LoadedNPCEvent(builderInfo));
            }
        }
    }
    
    protected boolean isDependant(@Nonnull final Builder<?> builder, final int builderIndex, final int dependencyIndex) {
        return builderIndex == dependencyIndex || this.computeAllDependencies(builder, builderIndex).contains(dependencyIndex);
    }
    
    protected int cacheBuilder(final String name, final Builder<?> builder, final Path path) {
        this.indexLock.writeLock().lock();
        int index;
        try {
            index = this.nameToIndexMap.getInt(name);
            if (index >= 0) {
                this.removeBuilder(index);
            }
            else {
                index = this.nextIndex.getAndIncrement();
                this.nameToIndexMap.put(name, index);
            }
        }
        finally {
            this.indexLock.writeLock().unlock();
        }
        final BuilderInfo builderInfo = new BuilderInfo(index, name, builder, path);
        this.builderCache.put(index, builderInfo);
        onBuilderAdded(builderInfo);
        return index;
    }
    
    private void removeBuilder(final int index) {
        final BuilderInfo builder = this.builderCache.remove(index);
        if (builder != null) {
            builder.setRemoved();
        }
    }
    
    private void removeBuilder(final String name) {
        final int index = this.getIndex(name);
        if (index >= 0) {
            this.removeBuilder(index);
        }
    }
    
    @Nullable
    private Builder<?> tryGetCachedBuilder(final int index) {
        final BuilderInfo entry = this.tryGetBuilderInfo(index);
        return (entry == null) ? null : entry.getBuilder();
    }
    
    @Nullable
    private BuilderInfo tryGetCachedBuilderInfo(final int index, @Nonnull final Class<?> classType) {
        final BuilderInfo entry = this.tryGetBuilderInfo(index);
        if (entry == null) {
            return null;
        }
        final Builder<?> cachedBuilder = entry.getBuilder();
        if (cachedBuilder.category() != classType) {
            throw new IllegalArgumentException(String.format("Asset '%s'(%s) is different type. Is '%s' but should be '%s'", this.lookupName(index), index, cachedBuilder.category().getName(), classType.getName()));
        }
        return entry;
    }
    
    private static void validateAllSpawnableNPCs(@Nonnull final Int2ObjectMap<BuilderInfo> builders, @Nonnull final List<String> errors) {
        builders.forEach((index, builderInfo) -> {
            final Builder<?> builder = builderInfo.getBuilder();
            if (builder.isSpawnable() && builder instanceof SpawnableWithModelBuilder) {
                final SpawnableWithModelBuilder<?> spawnableBuilder = (SpawnableWithModelBuilder)builder;
                final ExecutionContext context = new ExecutionContext(builder.getBuilderParameters().createScope());
                final String fileName = builderInfo.getPath().toString();
                String modelName;
                try {
                    modelName = spawnableBuilder.getSpawnModelName(context, spawnableBuilder.createModifierScope(context));
                }
                catch (final SkipSentryException | IllegalStateException e) {
                    errors.add(String.format("%s: %s", fileName, e.getMessage()));
                    builderInfo.setValidated(false);
                    return;
                }
                final ModelAsset modelAsset = ModelAsset.getAssetMap().getAsset(modelName);
                if (modelAsset == null) {
                    errors.add(String.format("%s: Model %s does not exist.", fileName, modelName));
                    builderInfo.setValidated(false);
                }
                else {
                    final Model model = Model.createScaledModel(modelAsset, modelAsset.getMaxScale());
                    final Builder<?> builderInstance = builderInfo.getBuilder();
                    new NPCLoadTimeValidationHelper(fileName, model, !builderInstance.isSpawnable());
                    final NPCLoadTimeValidationHelper npcLoadTimeValidationHelper;
                    final NPCLoadTimeValidationHelper validationHelper = npcLoadTimeValidationHelper;
                    if (!builderInstance.validate(fileName, validationHelper, context, context.getScope(), errors) || !validationHelper.validateMotionControllers(errors) || !validationHelper.getValueStoreValidator().validate(errors)) {
                        builderInfo.setValidated(false);
                    }
                }
            }
        });
    }
    
    private static void sendReloadNotification(final Message message, @Nonnull final Set<String> builders) {
        if (builders.isEmpty()) {
            return;
        }
        NotificationUtil.sendNotificationToUniverse(message, Message.raw(builders.toString()), NotificationStyle.Warning);
    }
    
    private static boolean isIgnoredFile(@Nonnull final Path path) {
        return !path.getFileName().toString().isEmpty() && path.getFileName().toString().charAt(0) == '!';
    }
    
    private static boolean isJsonFile(@Nonnull final Path path) {
        return Files.isRegularFile(path, new LinkOption[0]) && path.toString().endsWith(".json");
    }
    
    private static boolean isJsonFileName(@Nonnull final Path path, final EventKind eventKind) {
        return path.toString().endsWith(".json");
    }
    
    @Nonnull
    private static String builderNameFromPath(@Nonnull final Path path) {
        String fileName = path.getFileName().toString();
        if (fileName.startsWith("NPCRole-")) {
            fileName = fileName.split("-")[1];
        }
        final int endIndex = fileName.lastIndexOf(46);
        return (endIndex >= 0) ? fileName.substring(0, endIndex) : fileName;
    }
    
    @Nonnull
    private String buildPathString(@Nonnull final IntArrayList path, final int index) {
        if (path.isEmpty()) {
            return "";
        }
        final StringBuilder result = new StringBuilder();
        result.append(" (Path: ");
        final IntIterator i = path.iterator();
        while (i.hasNext()) {
            result.append(this.lookupName(i.nextInt())).append(" -> ");
        }
        result.append(this.lookupName(index)).append(')');
        return result.toString();
    }
    
    private boolean validateBuilder(@Nonnull final BuilderInfo builderInfo, @Nonnull final IntSet validatedDependencies, @Nonnull final IntArrayList path) {
        final int index = builderInfo.getIndex();
        if (path.contains(index)) {
            NPCPlugin.get().getLogger().at(Level.SEVERE).log("Builder '%s' validation failed: Cyclic reference detected for builder '%s'%s", this.lookupName(path.getInt(0)), this.lookupName(index), this.buildPathString(path, index));
            return builderInfo.setValidated(false);
        }
        path.add(index);
        IntSet dependencies;
        try {
            dependencies = this.computeAllDependencies(builderInfo.getBuilder(), builderInfo.getIndex());
        }
        catch (final SkipSentryException | IllegalStateException | IllegalArgumentException e) {
            NPCPlugin.get().getLogger().at(Level.SEVERE).log("Builder '%s' validation failed: %s", this.lookupName(path.getInt(0)), e.getMessage());
            return builderInfo.setValidated(false);
        }
        boolean valid = true;
        final IntIterator i = dependencies.iterator();
        while (i.hasNext()) {
            final int dependency = i.nextInt();
            if (path.contains(dependency)) {
                NPCPlugin.get().getLogger().at(Level.SEVERE).log("Builder '%s' validation failed: Cyclic reference detected for builder '%s'%s", this.lookupName(path.getInt(0)), this.lookupName(dependency), this.buildPathString(path, index));
                return builderInfo.setValidated(false);
            }
            if (!validatedDependencies.add(dependency)) {
                continue;
            }
            final BuilderInfo childBuilder = this.builderCache.get(dependency);
            if (childBuilder == null) {
                NPCPlugin.get().getLogger().at(Level.SEVERE).log("Builder '%s' validation failed: Reference to unknown builder '%s'%s", this.lookupName(path.getInt(0)), this.lookupName(dependency), this.buildPathString(path, dependency));
                valid = false;
            }
            else if (!childBuilder.isValidated()) {
                valid = this.validateBuilder(childBuilder, validatedDependencies, path);
            }
            else {
                if (childBuilder.isValid()) {
                    continue;
                }
                NPCPlugin.get().getLogger().at(Level.SEVERE).log("Builder '%s' validation failed: Reference to invalid builder '%s'%s", this.lookupName(path.getInt(0)), childBuilder.getKeyName(), this.buildPathString(path, dependency));
                valid = false;
            }
        }
        path.removeInt(path.size() - 1);
        return builderInfo.setValidated(valid);
    }
    
    @Nonnull
    private IntSet computeAllDependencies(@Nonnull final Builder<?> builder, final int builderIndex) {
        return this.computeAllDependencies(builder, builderIndex, new IntOpenHashSet(), new IntArrayList());
    }
    
    @Nonnull
    private IntSet computeAllDependencies(@Nonnull final Builder<?> builder, final int builderIndex, @Nonnull final IntSet dependencies, @Nonnull final IntArrayList path) {
        if (path.contains(builderIndex)) {
            throw new SkipSentryException(new IllegalArgumentException("Cyclic reference detected for builder: " + this.lookupName(builderIndex)));
        }
        path.add(builderIndex);
        this.iterateDependencies(builder.getDependencies().iterator(), dependencies, path);
        if (builder.hasDynamicDependencies()) {
            this.iterateDependencies(builder.getDynamicDependencies().iterator(), dependencies, path);
        }
        path.removeInt(path.size() - 1);
        return dependencies;
    }
    
    private void iterateDependencies(@Nonnull final IntIterator iterator, @Nonnull final IntSet dependencies, @Nonnull final IntArrayList path) {
        while (iterator.hasNext()) {
            final int dependency = iterator.nextInt();
            if (!dependencies.contains(dependency)) {
                dependencies.add(dependency);
                final Builder<?> child = this.tryGetCachedBuilder(dependency);
                if (child == null) {
                    throw new SkipSentryException(new IllegalStateException("Reference to unknown builder: " + this.lookupName(dependency)));
                }
                this.computeAllDependencies(child, dependency, dependencies, path);
            }
        }
    }
    
    private void reloadDependants(final int dependency) {
        for (final BuilderInfo builderInfo : this.builderCache.values()) {
            final int index = builderInfo.getIndex();
            final String keyName = builderInfo.getKeyName();
            final Builder<?> builder = builderInfo.getBuilder();
            try {
                if (!builder.isSpawnable() || !this.isDependant(builder, index, dependency)) {
                    continue;
                }
                NPCPlugin.get().getLogger().at(Level.INFO).log("Reloading entities of type '%s' because dependency '%s' changed", keyName, this.lookupName(dependency));
                onBuilderReloaded(builderInfo);
            }
            catch (final Throwable e) {
                NPCPlugin.get().getLogger().at(Level.INFO).log("Failed to reload entities of type '%s': %s", keyName, e.getMessage());
            }
        }
    }
    
    private enum TestType
    {
        NORMAL, 
        FAILING;
    }
    
    private class BuilderAssetMonitorHandler implements AssetMonitorHandler
    {
        private final AssetPack pack;
        private final boolean includeTests;
        
        public BuilderAssetMonitorHandler(final AssetPack pack, final boolean includeTests) {
            this.pack = pack;
            this.includeTests = includeTests;
        }
        
        @Override
        public Object getKey() {
            return this.pack;
        }
        
        @Override
        public boolean test(final Path path, final EventKind eventKind) {
            return BuilderManager.isJsonFileName(path, eventKind);
        }
        
        @Override
        public void accept(final Map<Path, EventKind> map) {
            final Int2ObjectOpenHashMap<BuilderInfo> loadedBuilders = new Int2ObjectOpenHashMap<BuilderInfo>();
            final HashSet<String> loadedBuilderNames = new HashSet<String>();
            final HashSet<String> failedBuilderTexts = new HashSet<String>();
            final HashSet<String> deletedBuilderNames = new HashSet<String>();
            final ObjectArrayList<String> errors = new ObjectArrayList<String>();
            for (Map.Entry<Path, EventKind> entry : map.entrySet()) {
                final Path path = entry.getKey();
                final EventKind eventKind = entry.getValue();
                if (eventKind == EventKind.ENTRY_CREATE || eventKind == EventKind.ENTRY_MODIFY) {
                    if (!Files.isRegularFile(path, new LinkOption[0]) || BuilderManager.isIgnoredFile(path)) {
                        continue;
                    }
                    try {
                        final int builderIndex = BuilderManager.this.loadFile(path, errors, null, this.includeTests, true);
                        if (builderIndex < 0) {
                            continue;
                        }
                        final String name = BuilderManager.builderNameFromPath(path);
                        NPCPlugin.get().getLogger().at(Level.INFO).log("Reloaded NPC builder " + name);
                        loadedBuilderNames.add(name);
                        for (final BuilderInfo builderInfo : BuilderManager.this.builderCache.values()) {
                            try {
                                if (!BuilderManager.this.isDependant(builderInfo.getBuilder(), builderInfo.getIndex(), builderIndex)) {
                                    continue;
                                }
                                builderInfo.setNeedsValidation();
                            }
                            catch (final IllegalStateException | IllegalArgumentException e) {
                                NPCPlugin.get().getLogger().at(Level.WARNING).log("Could not check if builder %s was dependent: %s", BuilderManager.this.lookupName(builderInfo.getIndex()), e.getMessage());
                            }
                        }
                        if (BuilderManager.this.autoReload) {
                            BuilderManager.this.reloadDependants(builderIndex);
                        }
                        final BuilderInfo builder = BuilderManager.this.builderCache.get(builderIndex);
                        BuilderManager.onBuilderReloaded(builder);
                        loadedBuilders.put(builderIndex, builder);
                    }
                    catch (final Throwable e2) {
                        NPCPlugin.get().getLogger().at(Level.SEVERE).log("Failed to reload %s config %s: %s", "NPC", path, e2.getMessage());
                        failedBuilderTexts.add(BuilderManager.builderNameFromPath(path) + ": " + e2.getMessage());
                    }
                }
                else {
                    if (eventKind != EventKind.ENTRY_DELETE) {
                        continue;
                    }
                    final String builderName = BuilderManager.builderNameFromPath(path);
                    BuilderManager.this.removeBuilder(builderName);
                    NPCPlugin.get().getLogger().at(Level.INFO).log("Deleted %s builder %s", "NPC", builderName);
                    deletedBuilderNames.add(builderName);
                }
            }
            BuilderManager.sendReloadNotification(Message.translation("server.general.assetstore.reloadAssets").param("class", "NPC"), loadedBuilderNames);
            BuilderManager.sendReloadNotification(Message.translation("server.general.assetstore.loadFailed").param("class", "NPC"), failedBuilderTexts);
            BuilderManager.sendReloadNotification(Message.translation("server.general.assetstore.removedAssets").param("class", "NPC"), deletedBuilderNames);
            BuilderManager.this.finishLoadingBuilders(loadedBuilders, errors);
        }
    }
}
