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

package com.hypixel.hytale.assetstore;

import com.hypixel.hytale.common.util.StringUtil;
import com.hypixel.hytale.logger.sentry.SkipSentryException;
import com.hypixel.hytale.logger.backend.HytaleLoggerBackend;
import com.hypixel.hytale.logger.util.GithubMessageUtil;
import com.hypixel.hytale.codec.exception.CodecValidationException;
import com.hypixel.hytale.codec.exception.CodecException;
import java.util.function.Supplier;
import java.util.concurrent.CompletableFuture;
import com.hypixel.hytale.assetstore.event.LoadedAssetsEvent;
import com.hypixel.hytale.assetstore.event.GenerateAssetsEvent;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.sneakythrow.SneakyThrow;
import com.hypixel.hytale.codec.util.RawJsonReader;
import org.bson.BsonDocument;
import java.nio.file.StandardOpenOption;
import java.nio.file.OpenOption;
import com.hypixel.hytale.event.IEventDispatcher;
import com.hypixel.hytale.event.IEvent;
import com.hypixel.hytale.assetstore.event.RemovedAssetsEvent;
import java.util.HashMap;
import com.hypixel.hytale.common.util.FormatUtil;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import java.util.Collection;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.SimpleFileVisitor;
import java.util.ArrayList;
import java.util.Objects;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.logging.Level;
import com.hypixel.hytale.assetstore.codec.ContainedAssetCodec;
import com.hypixel.hytale.codec.validation.validator.MapValueValidator;
import com.hypixel.hytale.codec.validation.validator.MapKeyValidator;
import com.hypixel.hytale.codec.validation.validator.ArrayValidator;
import com.hypixel.hytale.codec.validation.Validator;
import com.hypixel.hytale.codec.builder.BuilderField;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.codec.ExtraInfo;
import com.hypixel.hytale.codec.validation.ValidationResults;
import org.bson.BsonValue;
import org.bson.BsonString;
import com.hypixel.hytale.codec.Codec;
import javax.annotation.Nullable;
import java.util.Iterator;
import java.nio.file.Path;
import com.hypixel.hytale.event.IEventBus;
import java.util.Collections;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import java.util.List;
import java.util.function.Predicate;
import java.util.Set;
import java.util.function.Function;
import com.hypixel.hytale.assetstore.codec.AssetCodec;
import javax.annotation.Nonnull;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.assetstore.map.JsonAssetWithMap;

public abstract class AssetStore<K, T extends JsonAssetWithMap<K, M>, M extends AssetMap<K, T>>
{
    public static boolean DISABLE_ASSET_COMPARE;
    @Nonnull
    protected final HytaleLogger logger;
    @Nonnull
    protected final Class<K> kClass;
    @Nonnull
    protected final Class<T> tClass;
    protected final String path;
    @Nonnull
    protected final String extension;
    protected final AssetCodec<K, T> codec;
    protected final Function<T, K> keyFunction;
    @Nonnull
    protected final Set<Class<? extends JsonAsset<?>>> loadsAfter;
    @Nonnull
    protected final Set<Class<? extends JsonAsset<?>>> unmodifiableLoadsAfter;
    @Nonnull
    protected final Set<Class<? extends JsonAsset<?>>> loadsBefore;
    protected final M assetMap;
    protected final Function<K, T> replaceOnRemove;
    @Nonnull
    protected final Predicate<T> isUnknown;
    protected final boolean unmodifiable;
    protected final List<T> preAddedAssets;
    protected final Class<? extends JsonAsset<?>> idProvider;
    protected final Map<Class<? extends JsonAssetWithMap<?, ?>>, Map<K, Set<Object>>> childAssetsMap;
    @Nonnull
    protected Set<Class<? extends JsonAssetWithMap>> loadedContainedAssetsFor;
    public static boolean DISABLE_DYNAMIC_DEPENDENCIES;
    
    public AssetStore(@Nonnull final Builder<K, T, M, ?> builder) {
        this.childAssetsMap = new ConcurrentHashMap<Class<? extends JsonAssetWithMap<?, ?>>, Map<K, Set<Object>>>();
        this.loadedContainedAssetsFor = new HashSet<Class<? extends JsonAssetWithMap>>();
        this.kClass = builder.kClass;
        this.tClass = builder.tClass;
        this.logger = HytaleLogger.get("AssetStore|" + this.tClass.getSimpleName());
        this.path = builder.path;
        this.extension = builder.extension;
        this.codec = builder.codec;
        this.keyFunction = builder.keyFunction;
        this.isUnknown = ((builder.isUnknown == null) ? (v -> false) : builder.isUnknown);
        this.loadsAfter = builder.loadsAfter;
        this.unmodifiableLoadsAfter = Collections.unmodifiableSet((Set<? extends Class<? extends JsonAsset<?>>>)builder.loadsAfter);
        this.loadsBefore = Collections.unmodifiableSet((Set<? extends Class<? extends JsonAsset<?>>>)builder.loadsBefore);
        this.assetMap = builder.assetMap;
        this.replaceOnRemove = builder.replaceOnRemove;
        this.unmodifiable = builder.unmodifiable;
        this.preAddedAssets = builder.preAddedAssets;
        this.idProvider = builder.idProvider;
        if (builder.replaceOnRemove == null && this.assetMap.requireReplaceOnRemove()) {
            throw new IllegalArgumentException("AssetStore for " + this.tClass.getSimpleName() + " using an AssetMap of " + this.assetMap.getClass().getSimpleName() + " must use #setReplaceOnRemove");
        }
    }
    
    protected abstract IEventBus getEventBus();
    
    public abstract void addFileMonitor(@Nonnull final String p0, final Path p1);
    
    public abstract void removeFileMonitor(final Path p0);
    
    protected abstract void handleRemoveOrUpdate(final Set<K> p0, final Map<K, T> p1, @Nonnull final AssetUpdateQuery p2);
    
    @Nonnull
    public Class<K> getKeyClass() {
        return this.kClass;
    }
    
    @Nonnull
    public Class<T> getAssetClass() {
        return this.tClass;
    }
    
    public String getPath() {
        return this.path;
    }
    
    @Nonnull
    public String getExtension() {
        return this.extension;
    }
    
    public AssetCodec<K, T> getCodec() {
        return this.codec;
    }
    
    public Function<T, K> getKeyFunction() {
        return this.keyFunction;
    }
    
    @Nonnull
    public Set<Class<? extends JsonAsset<?>>> getLoadsAfter() {
        return this.unmodifiableLoadsAfter;
    }
    
    public M getAssetMap() {
        return this.assetMap;
    }
    
    public Function<K, T> getReplaceOnRemove() {
        return this.replaceOnRemove;
    }
    
    public boolean isUnmodifiable() {
        return this.unmodifiable;
    }
    
    public List<T> getPreAddedAssets() {
        return this.preAddedAssets;
    }
    
    public <X extends JsonAssetWithMap> boolean hasLoadedContainedAssetsFor(final Class<X> x) {
        return this.loadedContainedAssetsFor.contains(x);
    }
    
    public Class<? extends JsonAsset<?>> getIdProvider() {
        return this.idProvider;
    }
    
    @Nonnull
    public HytaleLogger getLogger() {
        return this.logger;
    }
    
    public void simplifyLoadBeforeDependencies() {
        for (final Class<? extends JsonAsset<?>> aClass : this.loadsBefore) {
            AssetRegistry.getAssetStore(aClass).loadsAfter.add(this.tClass);
        }
    }
    
    @Deprecated
    public <D extends JsonAsset<?>> void injectLoadsAfter(final Class<D> aClass) {
        if (AssetStore.DISABLE_DYNAMIC_DEPENDENCIES) {
            throw new IllegalArgumentException("Asset stores have already loaded! Injecting a dependency is now pointless.");
        }
        this.loadsAfter.add(aClass);
    }
    
    @Nullable
    public K decodeFilePathKey(@Nonnull final Path path) {
        final String fileName = path.getFileName().toString();
        return this.decodeStringKey(fileName.substring(0, fileName.length() - this.extension.length()));
    }
    
    @Nullable
    public K decodeStringKey(final String key) {
        if (this.codec.getKeyCodec().getChildCodec() == Codec.STRING) {
            return (K)key;
        }
        return this.codec.getKeyCodec().getChildCodec().decode(new BsonString(key));
    }
    
    @Nullable
    public K transformKey(@Nullable final Object o) {
        if (o == null) {
            return null;
        }
        if (o.getClass().equals(this.kClass)) {
            return (K)o;
        }
        return this.decodeStringKey(o.toString());
    }
    
    public void validate(@Nullable final K key, @Nonnull final ValidationResults results, final ExtraInfo extraInfo) {
        if (key == null) {
            return;
        }
        if (((AssetMap<K, JsonAsset>)this.assetMap).getAsset(key) != null) {
            return;
        }
        if (extraInfo instanceof final AssetExtraInfo assetExtraInfo) {
            for (AssetExtraInfo.Data data = assetExtraInfo.getData(); data != null; data = data.getContainerData()) {
                if (data.containsAsset(this.tClass, key)) {
                    return;
                }
            }
        }
        results.fail("Asset '" + String.valueOf(key) + "' of type " + this.tClass.getName() + " doesn't exist!");
    }
    
    public void validateCodecDefaults() {
        final ExtraInfo extraInfo = new ExtraInfo(Integer.MAX_VALUE, (Function<ExtraInfo, ValidationResults>)AssetValidationResults::new);
        this.codec.validateDefaults(extraInfo, new HashSet<Codec<?>>());
        extraInfo.getValidationResults().logOrThrowValidatorExceptions(this.logger, "Default Asset Validation Failed!\n");
    }
    
    public void logDependencies() {
        final ExtraInfo extraInfo = new ExtraInfo(Integer.MAX_VALUE, (Function<ExtraInfo, ValidationResults>)AssetValidationResults::new);
        final HashSet<Codec<?>> tested = new HashSet<Codec<?>>();
        this.codec.validateDefaults(extraInfo, tested);
        final Set<Class<? extends JsonAsset<?>>> assetClasses = new HashSet<Class<? extends JsonAsset<?>>>();
        final Set<Class<? extends JsonAsset<?>>> maybeLateAssetClasses = new HashSet<Class<? extends JsonAsset<?>>>();
        for (final Codec<?> other : tested) {
            if (other instanceof final BuilderCodec builderCodec2) {
                for (BuilderCodec<?> builderCodec = builderCodec2; builderCodec != null; builderCodec = builderCodec.getParent()) {
                    for (final List<? extends BuilderField<?, ?>> value : builderCodec.getEntries().values()) {
                        for (final BuilderField<?, ?> field : value) {
                            if (field.supportsVersion(extraInfo.getVersion())) {
                                final List<Validator<?>> validators = field.getValidators();
                                if (validators == null) {
                                    continue;
                                }
                                for (final Validator<?> validator2 : validators) {
                                    Validator<?> validator = validator2;
                                    if (validator2 instanceof ArrayValidator) {
                                        final ArrayValidator<?> arrayValidator = (ArrayValidator<?>)validator2;
                                        validator = arrayValidator.getValidator();
                                    }
                                    else {
                                        final Validator<?> validator3 = validator;
                                        if (validator3 instanceof MapKeyValidator) {
                                            final MapKeyValidator<?> arrayValidator2 = (MapKeyValidator<?>)validator3;
                                            validator = arrayValidator2.getKeyValidator();
                                        }
                                        else {
                                            final Validator<?> validator4 = validator;
                                            if (validator4 instanceof MapValueValidator) {
                                                final MapValueValidator<?> arrayValidator3 = (MapValueValidator<?>)validator4;
                                                validator = arrayValidator3.getValueValidator();
                                            }
                                        }
                                    }
                                    if (validator instanceof final AssetKeyValidator assetKeyValidator) {
                                        assetClasses.add((Class<? extends JsonAsset<?>>)assetKeyValidator.getStore().getAssetClass());
                                    }
                                }
                            }
                        }
                    }
                }
            }
            else {
                if (!(other instanceof ContainedAssetCodec)) {
                    continue;
                }
                final ContainedAssetCodec<?, ?, ?> containedAssetCodec = (ContainedAssetCodec)other;
                maybeLateAssetClasses.add((Class<? extends JsonAsset<?>>)containedAssetCodec.getAssetClass());
            }
        }
        final HashSet<Object> missing = new HashSet<Object>();
        final HashSet<Object> unused = new HashSet<Object>();
        for (final Class<? extends JsonAsset<?>> assetClass : assetClasses) {
            if (!this.loadsAfter.contains(assetClass)) {
                missing.add(assetClass);
            }
        }
        for (final Class<? extends JsonAsset<?>> aClass : this.loadsAfter) {
            if (!assetClasses.contains(aClass) && !maybeLateAssetClasses.contains(aClass)) {
                unused.add(aClass);
            }
        }
        if (!missing.isEmpty()) {
            this.logger.at(Level.WARNING).log("\nMissing Dependencies:" + (String)missing.stream().map((Function<? super Object, ?>)Object::toString).collect((Collector<? super Object, ?, String>)Collectors.joining("\n- ", "\n- ", "")));
        }
        if (!unused.isEmpty()) {
            this.logger.at(Level.WARNING).log("\nUnused Dependencies:" + (String)unused.stream().map((Function<? super Object, ?>)Object::toString).collect((Collector<? super Object, ?, String>)Collectors.joining("\n- ", "\n- ", "")));
        }
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssetsFromDirectory(@Nonnull final String packKey, @Nonnull final Path assetsPath) throws IOException {
        if (this.unmodifiable) {
            throw new UnsupportedOperationException("AssetStore is unmodifiable!");
        }
        Objects.requireNonNull(assetsPath, "assetsPath can't be null");
        final ArrayList<Path> files = new ArrayList<Path>();
        final Set<FileVisitOption> optionsSet = Set.of();
        Files.walkFileTree(assetsPath, optionsSet, Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
            @Nonnull
            @Override
            public FileVisitResult visitFile(@Nonnull final Path file, @Nonnull final BasicFileAttributes attrs) throws IOException {
                if (attrs.isRegularFile() && file.toString().endsWith(AssetStore.this.extension)) {
                    files.add(file);
                }
                return FileVisitResult.CONTINUE;
            }
        });
        return this.loadAssetsFromPaths(packKey, files);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssetsFromPaths(@Nonnull final String packKey, @Nonnull final List<Path> paths) {
        return this.loadAssetsFromPaths(packKey, paths, AssetUpdateQuery.DEFAULT);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssetsFromPaths(@Nonnull final String packKey, @Nonnull final Collection<Path> paths, @Nonnull final AssetUpdateQuery query) {
        return this.loadAssetsFromPaths(packKey, paths, query, false);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssetsFromPaths(@Nonnull final String packKey, @Nonnull final Collection<Path> paths, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll) {
        if (this.unmodifiable) {
            throw new UnsupportedOperationException("AssetStore is unmodifiable!");
        }
        Objects.requireNonNull(paths, "paths can't be null");
        final long start = System.nanoTime();
        final Set<Path> documents = new HashSet<Path>();
        for (final Path path : paths) {
            final Path normalize = path.toAbsolutePath().normalize();
            final Set<K> keys = ((AssetMap<K, T>)this.assetMap).getKeys(normalize);
            if (keys != null) {
                for (final K key : keys) {
                    this.loadAllChildren(documents, key);
                }
            }
            documents.add(normalize);
            this.loadAllChildren(documents, this.decodeFilePathKey(path));
        }
        final List<RawAsset<K>> rawAssets = new ArrayList<RawAsset<K>>(documents.size());
        for (final Path p : documents) {
            rawAssets.add(new RawAsset<K>(this.decodeFilePathKey(p), p));
        }
        final Map<K, T> loadedAssets = Collections.synchronizedMap(new Object2ObjectLinkedOpenHashMap<K, T>());
        final Map<K, Path> loadedKeyToPathMap = new ConcurrentHashMap<K, Path>();
        final Set<K> failedToLoadKeys = (Set<K>)ConcurrentHashMap.newKeySet();
        final Set<Path> failedToLoadPaths = (Set<Path>)ConcurrentHashMap.newKeySet();
        final Map<Class<? extends JsonAssetWithMap>, AssetLoadResult> childAssetResults = new ConcurrentHashMap<Class<? extends JsonAssetWithMap>, AssetLoadResult>();
        this.loadAssets0(packKey, loadedAssets, rawAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, query, forceLoadAll, childAssetResults);
        final long end = System.nanoTime();
        final long diff = end - start;
        this.logger.at(Level.FINE).log("Loaded %d and removed %s (%s total) of %s from %s files in %s", loadedAssets.size(), failedToLoadKeys.size(), this.assetMap.getAssetCount(), this.tClass.getSimpleName(), paths.size(), FormatUtil.nanosToString(diff));
        return new AssetLoadResult<K, T>(loadedAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, (Map<Class<? extends JsonAssetWithMap>, AssetLoadResult<K, T>>)childAssetResults);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadBuffersWithKeys(@Nonnull final String packKey, @Nonnull final List<RawAsset<K>> preLoaded, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll) {
        final long start = System.nanoTime();
        final Set<Path> documents = new HashSet<Path>();
        for (final RawAsset<K> document : preLoaded) {
            this.loadAllChildren(documents, document.getKey());
        }
        final List<RawAsset<K>> rawAssets = new ArrayList<RawAsset<K>>(preLoaded.size() + documents.size());
        rawAssets.addAll(preLoaded);
        for (final Path p : documents) {
            rawAssets.add(new RawAsset<K>(this.decodeFilePathKey(p), p));
        }
        final Map<K, T> loadedAssets = Collections.synchronizedMap(new Object2ObjectLinkedOpenHashMap<K, T>());
        final Map<K, Path> loadedKeyToPathMap = new ConcurrentHashMap<K, Path>();
        final Set<K> failedToLoadKeys = (Set<K>)ConcurrentHashMap.newKeySet();
        final Set<Path> failedToLoadPaths = (Set<Path>)ConcurrentHashMap.newKeySet();
        final Map<Class<? extends JsonAssetWithMap>, AssetLoadResult> childAssetResults = new ConcurrentHashMap<Class<? extends JsonAssetWithMap>, AssetLoadResult>();
        this.loadAssets0(packKey, loadedAssets, rawAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, query, forceLoadAll, childAssetResults);
        final long end = System.nanoTime();
        final long diff = end - start;
        this.logger.at(Level.FINE).log("Loaded %d and removed %s (%s total) of %s via loadBuffersWithKeys in %s", loadedAssets.size(), failedToLoadKeys.size(), this.assetMap.getAssetCount(), this.tClass.getSimpleName(), FormatUtil.nanosToString(diff));
        return new AssetLoadResult<K, T>(loadedAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, (Map<Class<? extends JsonAssetWithMap>, AssetLoadResult<K, T>>)childAssetResults);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssets(@Nonnull final String packKey, @Nonnull final List<T> assets) {
        return this.loadAssets(packKey, assets, AssetUpdateQuery.DEFAULT);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssets(@Nonnull final String packKey, @Nonnull final List<T> assets, @Nonnull final AssetUpdateQuery query) {
        return this.loadAssets(packKey, assets, query, false);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssets(@Nonnull final String packKey, @Nonnull final List<T> assets, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll) {
        if (this.unmodifiable) {
            throw new UnsupportedOperationException("AssetStore is unmodifiable!");
        }
        Objects.requireNonNull(assets, "assets can't be null");
        final long start = System.nanoTime();
        final Map<K, T> loadedAssets = Collections.synchronizedMap(new Object2ObjectLinkedOpenHashMap<K, T>());
        final Set<Path> documents = new HashSet<Path>();
        this.loadAllChildren(loadedAssets, assets, documents);
        final List<RawAsset<K>> rawAssets = new ArrayList<RawAsset<K>>(documents.size());
        for (final Path p : documents) {
            rawAssets.add(new RawAsset<K>(this.decodeFilePathKey(p), p));
        }
        final Map<K, Path> loadedKeyToPathMap = new ConcurrentHashMap<K, Path>();
        final Set<K> failedToLoadKeys = (Set<K>)ConcurrentHashMap.newKeySet();
        final Set<Path> failedToLoadPaths = (Set<Path>)ConcurrentHashMap.newKeySet();
        final Map<Class<? extends JsonAssetWithMap>, AssetLoadResult> childAssetResults = new ConcurrentHashMap<Class<? extends JsonAssetWithMap>, AssetLoadResult>();
        this.loadAssets0(packKey, loadedAssets, rawAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, query, forceLoadAll, childAssetResults);
        final long end = System.nanoTime();
        final long diff = end - start;
        this.logger.at(Level.FINE).log("Loaded %d and removed %s (%s total) of %s via loadAssets in %s", loadedAssets.size(), failedToLoadKeys.size(), this.assetMap.getAssetCount(), this.tClass.getSimpleName(), FormatUtil.nanosToString(diff));
        return new AssetLoadResult<K, T>(loadedAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, (Map<Class<? extends JsonAssetWithMap>, AssetLoadResult<K, T>>)childAssetResults);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssetsWithReferences(@Nonnull final String packKey, @Nonnull final Map<T, List<AssetReferences<?, ?>>> assets) {
        return this.loadAssetsWithReferences(packKey, assets, AssetUpdateQuery.DEFAULT);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssetsWithReferences(@Nonnull final String packKey, @Nonnull final Map<T, List<AssetReferences<?, ?>>> assets, @Nonnull final AssetUpdateQuery query) {
        return this.loadAssetsWithReferences(packKey, assets, query, false);
    }
    
    @Nonnull
    public AssetLoadResult<K, T> loadAssetsWithReferences(@Nonnull final String packKey, @Nonnull final Map<T, List<AssetReferences<?, ?>>> assets, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll) {
        if (this.unmodifiable) {
            throw new UnsupportedOperationException("AssetStore is unmodifiable!");
        }
        Objects.requireNonNull(assets, "assets can't be null");
        final long start = System.nanoTime();
        final Map<K, T> loadedAssets = Collections.synchronizedMap(new Object2ObjectLinkedOpenHashMap<K, T>());
        final Set<T> assetKeys = assets.keySet();
        final Set<Path> documents = new HashSet<Path>();
        this.loadAllChildren(loadedAssets, assetKeys, documents);
        final List<RawAsset<K>> rawAssets = new ArrayList<RawAsset<K>>(documents.size());
        for (final Path p : documents) {
            rawAssets.add(new RawAsset<K>(this.decodeFilePathKey(p), p));
        }
        final Map<K, Path> loadedKeyToPathMap = new ConcurrentHashMap<K, Path>();
        final Set<K> failedToLoadKeys = (Set<K>)ConcurrentHashMap.newKeySet();
        final Set<Path> failedToLoadPaths = (Set<Path>)ConcurrentHashMap.newKeySet();
        final Map<Class<? extends JsonAssetWithMap>, AssetLoadResult> childAssetResults = new ConcurrentHashMap<Class<? extends JsonAssetWithMap>, AssetLoadResult>();
        this.loadAssets0(packKey, loadedAssets, rawAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, query, forceLoadAll, childAssetResults);
        for (final Map.Entry<T, List<AssetReferences<?, ?>>> entry : assets.entrySet()) {
            final T asset = entry.getKey();
            Objects.requireNonNull(asset, "asset can't be null");
            final K key = this.keyFunction.apply(asset);
            if (key == null) {
                throw new NullPointerException(String.format("key can't be null: %s", asset));
            }
            for (final AssetReferences<?, ?> references : entry.getValue()) {
                references.addChildAssetReferences(this.tClass, key);
            }
        }
        final long end = System.nanoTime();
        final long diff = end - start;
        this.logger.at(Level.FINE).log("Loaded %d and removed %s (%s total) of %s via loadAssetsWithReferences in %s", loadedAssets.size(), failedToLoadKeys.size(), this.assetMap.getAssetCount(), this.tClass.getSimpleName(), FormatUtil.nanosToString(diff));
        return new AssetLoadResult<K, T>(loadedAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, (Map<Class<? extends JsonAssetWithMap>, AssetLoadResult<K, T>>)childAssetResults);
    }
    
    @Nonnull
    public Set<K> removeAssetWithPaths(@Nonnull final String packKey, @Nonnull final List<Path> paths) {
        return this.removeAssetWithPaths(packKey, paths, AssetUpdateQuery.DEFAULT);
    }
    
    @Nonnull
    public Set<K> removeAssetWithPaths(@Nonnull final String packKey, @Nonnull final List<Path> paths, @Nonnull final AssetUpdateQuery assetUpdateQuery) {
        if (this.unmodifiable) {
            throw new UnsupportedOperationException("AssetStore is unmodifiable!");
        }
        final Set<K> allKeys = new HashSet<K>();
        for (final Path path : paths) {
            final Path normalize = path.toAbsolutePath().normalize();
            final Set<K> keys = ((AssetMap<K, T>)this.assetMap).getKeys(normalize);
            if (keys != null) {
                allKeys.addAll((Collection<? extends K>)keys);
            }
        }
        return this.removeAssets(packKey, false, allKeys, assetUpdateQuery);
    }
    
    @Nonnull
    public Set<K> removeAssetWithPath(final Path path) {
        return this.removeAssetWithPath(path, AssetUpdateQuery.DEFAULT);
    }
    
    @Nonnull
    public Set<K> removeAssetWithPath(final Path path, @Nonnull final AssetUpdateQuery assetUpdateQuery) {
        if (this.unmodifiable) {
            throw new UnsupportedOperationException("AssetStore is unmodifiable!");
        }
        final Path normalize = path.toAbsolutePath().normalize();
        final Set<K> keys = ((AssetMap<K, T>)this.assetMap).getKeys(normalize);
        if (keys != null) {
            return this.removeAssets("Hytale:Hytale", true, keys, assetUpdateQuery);
        }
        return Collections.emptySet();
    }
    
    @Nonnull
    public Set<K> removeAssets(@Nonnull final Collection<K> keys) {
        return this.removeAssets("Hytale:Hytale", true, keys, AssetUpdateQuery.DEFAULT);
    }
    
    @Nonnull
    public Set<K> removeAssets(@Nonnull final String packKey, final boolean all, @Nonnull final Collection<K> keys, @Nonnull final AssetUpdateQuery assetUpdateQuery) {
        if (this.unmodifiable) {
            throw new UnsupportedOperationException("AssetStore is unmodifiable!");
        }
        final long start = System.nanoTime();
        AssetRegistry.ASSET_LOCK.writeLock().lock();
        try {
            final Set<K> toBeRemoved = new HashSet<K>();
            final Set<K> temp = new HashSet<K>();
            final Iterator<K> iterator = keys.iterator();
            K key = null;
            while (iterator.hasNext()) {
                key = iterator.next();
                toBeRemoved.add(key);
                final Path path = ((AssetMap<K, T>)this.assetMap).getPath(key);
                if (path != null) {
                    this.logRemoveAsset(key, path);
                }
                else {
                    this.logRemoveAsset(key, null);
                }
                temp.clear();
                this.collectAllChildren(key, temp);
                this.logRemoveChildren(key, temp);
                toBeRemoved.addAll((Collection<? extends K>)temp);
            }
            if (toBeRemoved.isEmpty()) {
                return toBeRemoved;
            }
            this.removeChildrenAssets(packKey, toBeRemoved);
            List<Map.Entry<String, Object>> pathsToReload = null;
            if (all) {
                ((AssetMap<K, T>)this.assetMap).remove(toBeRemoved);
            }
            else {
                pathsToReload = new ArrayList<Map.Entry<String, Object>>();
                ((AssetMap<K, T>)this.assetMap).remove(packKey, toBeRemoved, pathsToReload);
            }
            if (this.replaceOnRemove != null) {
                final Map<K, T> replacements = toBeRemoved.stream().collect(Collectors.toMap((Function<? super Object, ? extends K>)Function.identity(), key -> {
                    final T replacement = this.replaceOnRemove.apply((K)key);
                    Objects.requireNonNull(replacement, "Replacement can't be null!");
                    final K replacementKey = this.keyFunction.apply(replacement);
                    if (replacementKey == null) {
                        throw new NullPointerException(key.toString());
                    }
                    else {
                        if (!key.equals(replacementKey)) {
                            this.logger.at(Level.WARNING).log("Replacement key '%s' doesn't match key '%s'", replacementKey, key);
                        }
                        return replacement;
                    }
                }));
                this.assetMap.putAll("Hytale:Hytale", this.codec, replacements, Collections.emptyMap(), Collections.emptyMap());
                this.handleRemoveOrUpdate(null, replacements, AssetUpdateQuery.DEFAULT);
                this.loadContainedAssets("Hytale:Hytale", replacements.values(), new HashMap<Class<? extends JsonAssetWithMap>, AssetLoadResult>(), AssetUpdateQuery.DEFAULT, false);
            }
            else {
                this.handleRemoveOrUpdate(toBeRemoved, null, assetUpdateQuery);
            }
            if (pathsToReload != null) {
                for (final Map.Entry<String, Object> e : pathsToReload) {
                    if (e.getValue() instanceof Path) {
                        this.loadAssetsFromPaths(e.getKey(), List.of(e.getValue()));
                    }
                    else {
                        this.loadAssets(e.getKey(), List.of(e.getValue()));
                    }
                }
            }
            final long end = System.nanoTime();
            final long diff = end - start;
            this.logger.at(Level.INFO).log("Removed %d (%s total) of %s via removeAssets in %s", toBeRemoved.size(), this.assetMap.getAssetCount(), this.tClass.getSimpleName(), FormatUtil.nanosToString(diff));
            if (!toBeRemoved.isEmpty()) {
                final IEventDispatcher dispatcher = this.getEventBus().dispatchFor((Class<? super IEvent>)RemovedAssetsEvent.class, this.tClass);
                if (dispatcher.hasListener()) {
                    dispatcher.dispatch(new RemovedAssetsEvent(this.tClass, this.assetMap, toBeRemoved, this.replaceOnRemove != null));
                }
            }
            return toBeRemoved;
        }
        finally {
            AssetRegistry.ASSET_LOCK.writeLock().unlock();
        }
    }
    
    public void removeAssetPack(@Nonnull final String name) {
        AssetRegistry.ASSET_LOCK.writeLock().lock();
        try {
            final Set<K> assets = ((AssetMap<K, T>)this.assetMap).getKeysForPack(name);
            if (assets == null) {
                return;
            }
            this.removeAssets(name, false, assets, AssetUpdateQuery.DEFAULT);
        }
        finally {
            AssetRegistry.ASSET_LOCK.writeLock().unlock();
        }
    }
    
    public AssetLoadResult<K, T> writeAssetToDisk(@Nonnull final AssetPack pack, @Nonnull final Map<Path, T> assetsByPath) throws IOException {
        return this.writeAssetToDisk(pack, assetsByPath, AssetUpdateQuery.DEFAULT);
    }
    
    public AssetLoadResult<K, T> writeAssetToDisk(@Nonnull final AssetPack pack, @Nonnull final Map<Path, T> assetsByPath, @Nonnull final AssetUpdateQuery query) throws IOException {
        if (pack.isImmutable()) {
            throw new IOException("Pack is immutable");
        }
        for (final Map.Entry<Path, T> entry : assetsByPath.entrySet()) {
            final T asset = entry.getValue();
            final K id = asset.getId();
            final Path assetPath = pack.getRoot().resolve("Server").resolve(this.path).resolve(entry.getKey());
            final AssetExtraInfo.Data data = this.codec.getData(asset);
            final Object parentId = (data == null) ? null : data.getParentKey();
            final BsonValue bsonValue = this.codec.encode(asset, new AssetExtraInfo<Object>(assetPath, new AssetExtraInfo.Data((Class<? extends JsonAsset<K>>)this.tClass, (K)id, this.transformKey(parentId))));
            Files.writeString(assetPath, bsonValue.toString(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
        }
        return this.loadAssets(pack.getName(), new ArrayList<T>((Collection<? extends T>)assetsByPath.values()), query);
    }
    
    @Nonnull
    public T decode(@Nonnull final String packKey, @Nonnull final K key, @Nonnull final BsonDocument document) {
        final KeyedCodec<K> parentCodec = this.codec.getParentCodec();
        final K parentKey = (parentCodec != null) ? parentCodec.getOrNull(document) : null;
        final RawJsonReader reader = RawJsonReader.fromBuffer(document.toString().toCharArray());
        try {
            final AssetExtraInfo<K> extraInfo = new AssetExtraInfo<K>(new AssetExtraInfo.Data(this.getAssetClass(), (K)key, (K)parentKey));
            if (parentKey == null) {
                reader.consumeWhiteSpace();
                final T asset = this.codec.decodeJsonAsset(reader, extraInfo);
                if (asset == null) {
                    throw new NullPointerException(document.toString());
                }
                extraInfo.getValidationResults().logOrThrowValidatorExceptions(this.logger);
                this.logUnusedKeys(key, null, extraInfo);
                return asset;
            }
            else {
                final T parent = parentKey.equals("super") ? this.assetMap.getAsset(packKey, key) : this.assetMap.getAsset(parentKey);
                if (parent == null) {
                    throw new NullPointerException(parentKey.toString());
                }
                reader.consumeWhiteSpace();
                final T asset2 = this.codec.decodeAndInheritJsonAsset(reader, parent, extraInfo);
                if (asset2 == null) {
                    throw new NullPointerException(document.toString());
                }
                extraInfo.getValidationResults().logOrThrowValidatorExceptions(this.logger);
                this.logUnusedKeys(key, null, extraInfo);
                return asset2;
            }
        }
        catch (final IOException e) {
            throw SneakyThrow.sneakyThrow(e);
        }
    }
    
    public <CK> void addChildAssetReferences(final K parentKey, final Class<? extends JsonAssetWithMap<CK, ?>> childAssetClass, @Nonnull final Set<CK> childKeys) {
        ((Set)this.childAssetsMap.computeIfAbsent((Class<? extends JsonAssetWithMap<?, ?>>)childAssetClass, k -> new ConcurrentHashMap()).computeIfAbsent((Object)parentKey, k -> ConcurrentHashMap.newKeySet())).addAll(childKeys);
    }
    
    protected void loadAssets0(@Nonnull final String packKey, @Nonnull final Map<K, T> loadedAssets, @Nonnull final List<RawAsset<K>> preLoaded, @Nonnull final Map<K, Path> loadedKeyToPathMap, @Nonnull final Set<K> failedToLoadKeys, @Nonnull final Set<Path> failedToLoadPaths, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll, @Nonnull final Map<Class<? extends JsonAssetWithMap>, AssetLoadResult> childAssetResults) {
        final Map<K, Set<K>> loadedAssetChildren = new ConcurrentHashMap<K, Set<K>>();
        this.decodeAssets(packKey, preLoaded, loadedAssets, loadedKeyToPathMap, loadedAssetChildren, failedToLoadKeys, failedToLoadPaths, this.assetMap, query, forceLoadAll);
        AssetRegistry.ASSET_LOCK.writeLock().lock();
        try {
            final IEventDispatcher generateDispatcher = this.getEventBus().dispatchFor((Class<? super IEvent>)GenerateAssetsEvent.class, this.tClass);
            if (generateDispatcher.hasListener()) {
                generateDispatcher.dispatch(new GenerateAssetsEvent(this.tClass, this.assetMap, loadedAssets, loadedAssetChildren));
            }
            final Map<K, K> toBeRemovedMap = new HashMap<K, K>();
            final Set<K> temp = new HashSet<K>();
            final Iterator<K> iterator = failedToLoadKeys.iterator();
            K key = null;
            K k = null;
            while (iterator.hasNext()) {
                key = iterator.next();
                if (toBeRemovedMap.putIfAbsent(key, key) == null) {
                    this.logRemoveAsset(key, null);
                    temp.clear();
                    this.collectAllChildren(key, temp);
                    final Iterator<K> iterator2 = temp.iterator();
                    while (iterator2.hasNext()) {
                        k = iterator2.next();
                        toBeRemovedMap.putIfAbsent(k, key);
                    }
                }
            }
            for (final Path path : failedToLoadPaths) {
                final Set<K> keys = ((AssetMap<K, T>)this.assetMap).getKeys(path);
                if (keys == null) {
                    continue;
                }
                for (final K key2 : keys) {
                    if (toBeRemovedMap.putIfAbsent(key2, key2) == null) {
                        this.logRemoveAsset(key2, path);
                        temp.clear();
                        this.collectAllChildren(key2, temp);
                        for (final K i : temp) {
                            toBeRemovedMap.putIfAbsent(i, key2);
                        }
                    }
                }
            }
            this.assetMap.putAll(packKey, this.codec, loadedAssets, loadedKeyToPathMap, loadedAssetChildren);
            final Set<K> toBeRemoved = toBeRemovedMap.keySet();
            if (!toBeRemoved.isEmpty()) {
                this.logRemoveChildren(toBeRemovedMap);
                this.removeChildrenAssets(packKey, toBeRemoved);
            }
            if (this.replaceOnRemove != null && !toBeRemoved.isEmpty()) {
                final Map<K, T> replacements = toBeRemoved.stream().filter(k -> ((AssetMap<K, JsonAsset>)this.assetMap).getAsset((K)k) != null).collect(Collectors.toMap((Function<? super Object, ? extends K>)Function.identity(), key -> {
                    final T replacement = this.replaceOnRemove.apply((K)key);
                    Objects.requireNonNull(replacement, "Replacement can't be null!");
                    final K replacementKey = this.keyFunction.apply(replacement);
                    if (replacementKey == null) {
                        throw new NullPointerException(key.toString());
                    }
                    else {
                        if (!key.equals(replacementKey)) {
                            this.logger.at(Level.WARNING).log("Replacement key '%s' doesn't match key '%s'", replacementKey, key);
                        }
                        return replacement;
                    }
                }));
                this.assetMap.putAll("Hytale:Hytale", this.codec, replacements, Collections.emptyMap(), Collections.emptyMap());
                replacements.putAll((Map<? extends K, ? extends T>)loadedAssets);
                this.handleRemoveOrUpdate(null, replacements, query);
            }
            else {
                ((AssetMap<K, T>)this.assetMap).remove(toBeRemoved);
                this.handleRemoveOrUpdate(toBeRemoved, loadedAssets, query);
            }
            this.loadContainedAssets(packKey, loadedAssets.values(), childAssetResults, query, forceLoadAll);
            this.reloadChildrenContainerAssets(packKey, loadedAssets);
            if (!loadedAssets.isEmpty()) {
                final IEventDispatcher dispatcher = this.getEventBus().dispatchFor((Class<? super IEvent>)LoadedAssetsEvent.class, this.tClass);
                if (dispatcher.hasListener()) {
                    dispatcher.dispatch(new LoadedAssetsEvent(this.tClass, this.assetMap, loadedAssets, false, query));
                }
            }
            if (!toBeRemoved.isEmpty()) {
                final IEventDispatcher dispatcher = this.getEventBus().dispatchFor((Class<? super IEvent>)RemovedAssetsEvent.class, this.tClass);
                if (dispatcher.hasListener()) {
                    dispatcher.dispatch(new RemovedAssetsEvent(this.tClass, this.assetMap, toBeRemoved, this.replaceOnRemove != null));
                }
            }
        }
        finally {
            AssetRegistry.ASSET_LOCK.writeLock().unlock();
        }
    }
    
    private void reloadChildrenContainerAssets(@Nonnull final String packKey, @Nonnull final Map<K, T> loadedAssets) {
        final HashSet<Path> toReload = new HashSet<Path>();
        final HashMap<Class<? extends JsonAssetWithMap<?, ?>>, Set<Path>> toReloadTypes = new HashMap<Class<? extends JsonAssetWithMap<?, ?>>, Set<Path>>();
        for (final Map.Entry<K, T> entry : loadedAssets.entrySet()) {
            final K key = entry.getKey();
            final Path path = ((AssetMap<K, T>)this.assetMap).getPath(key);
            if (path == null) {
                continue;
            }
            this.collectChildrenInDifferentFile(key, path, toReload, toReloadTypes, loadedAssets.keySet());
        }
        AssetUpdateQuery query = null;
        if (!toReload.isEmpty()) {
            query = new AssetUpdateQuery(true, AssetUpdateQuery.RebuildCache.DEFAULT);
            this.loadAssetsFromPaths(packKey, toReload, query, true);
        }
        if (!toReloadTypes.isEmpty()) {
            if (query == null) {
                query = new AssetUpdateQuery(true, AssetUpdateQuery.RebuildCache.DEFAULT);
            }
            for (final Map.Entry<Class<? extends JsonAssetWithMap<?, ?>>, Set<Path>> entry2 : toReloadTypes.entrySet()) {
                final AssetStore assetStore = AssetRegistry.getAssetStore(entry2.getKey());
                assetStore.loadAssetsFromPaths(packKey, entry2.getValue(), query, true);
            }
        }
    }
    
    private void collectChildrenInDifferentFile(final K key, @Nonnull final Path path, @Nonnull final Set<Path> paths, @Nonnull final Map<Class<? extends JsonAssetWithMap<?, ?>>, Set<Path>> typedPaths, @Nonnull final Set<K> ignore) {
        final Set<K> children = ((AssetMap<K, T>)this.assetMap).getChildren(key);
        for (final K child : children) {
            if (ignore.contains(child)) {
                continue;
            }
            final Path childPath = ((AssetMap<K, T>)this.assetMap).getPath(child);
            if (childPath != null && !path.equals(childPath)) {
                paths.add(childPath);
            }
            else {
                final AssetExtraInfo.Data data = this.codec.getData(this.assetMap.getAsset(child));
                final AssetExtraInfo.Data root = (data != null) ? data.getRootContainerData() : null;
                if (root != null) {
                    if (root.getAssetClass() == this.tClass) {
                        final K rootKey = (K)root.getKey();
                        if (ignore.contains(rootKey)) {
                            continue;
                        }
                        final Path rootPath = ((AssetMap<K, T>)this.assetMap).getPath(rootKey);
                        if (!path.equals(rootPath)) {
                            paths.add(rootPath);
                            continue;
                        }
                    }
                    else {
                        final Class assetClass = root.getAssetClass();
                        if (assetClass == null) {
                            continue;
                        }
                        final AssetStore assetStore = AssetRegistry.getAssetStore((Class<JsonAssetWithMap>)assetClass);
                        final Path rootPath2 = assetStore.getAssetMap().getPath(root.getKey());
                        if (rootPath2 != null) {
                            typedPaths.computeIfAbsent(assetClass, k -> new HashSet()).add(rootPath2);
                            continue;
                        }
                    }
                }
                this.collectChildrenInDifferentFile(child, path, paths, typedPaths, ignore);
            }
        }
    }
    
    protected void removeChildrenAssets(@Nonnull final String packKey, @Nonnull final Set<K> toBeRemoved) {
        for (final Map.Entry<Class<? extends JsonAssetWithMap<?, ?>>, Map<K, Set<Object>>> entry : this.childAssetsMap.entrySet()) {
            final Class k = entry.getKey();
            final Map<K, Set<Object>> value = entry.getValue();
            Set<Object> allChildKeys = null;
            for (final K key : toBeRemoved) {
                final Set<Object> childKeys = value.remove(key);
                if (childKeys != null) {
                    if (allChildKeys == null) {
                        allChildKeys = new HashSet<Object>();
                    }
                    allChildKeys.addAll(childKeys);
                }
            }
            if (allChildKeys != null && !allChildKeys.isEmpty()) {
                AssetRegistry.getAssetStore((Class<JsonAssetWithMap>)k).removeAssets(packKey, false, allChildKeys, AssetUpdateQuery.DEFAULT);
            }
        }
    }
    
    protected void loadContainedAssets(@Nonnull final String packKey, @Nonnull final Collection<T> assets, @Nonnull final Map<Class<? extends JsonAssetWithMap>, AssetLoadResult> childAssetsResults, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll) {
        final Map<Class<? extends JsonAssetWithMap>, Map<K, List<Object>>> containedAssetsByClass = new HashMap<Class<? extends JsonAssetWithMap>, Map<K, List<Object>>>();
        for (final T t : assets) {
            final AssetExtraInfo.Data data = this.codec.getData(t);
            if (data != null) {
                data.fetchContainedAssets(this.keyFunction.apply(t), containedAssetsByClass);
            }
        }
        for (final Map.Entry<Class<? extends JsonAssetWithMap>, Map<K, List<Object>>> entry : containedAssetsByClass.entrySet()) {
            final Class<? extends JsonAssetWithMap> assetClass = entry.getKey();
            final Map<K, List<Object>> containedAssets = entry.getValue();
            final AssetStore assetStore = AssetRegistry.getAssetStore(assetClass);
            this.loadedContainedAssetsFor.add(assetClass);
            final List<Object> childList = new ArrayList<Object>();
            for (final Map.Entry<K, List<Object>> containedEntry : containedAssets.entrySet()) {
                final K key = containedEntry.getKey();
                for (final Object contained : containedEntry.getValue()) {
                    final Object containedKey = assetStore.getKeyFunction().apply(contained);
                    this.childAssetsMap.computeIfAbsent(assetStore.getAssetClass(), k -> new ConcurrentHashMap()).computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet()).add(containedKey);
                    childList.add(contained);
                }
            }
            final AssetLoadResult result = assetStore.loadAssets(packKey, childList, query, forceLoadAll);
            childAssetsResults.put(assetClass, result);
        }
        final Map<Class<? extends JsonAssetWithMap>, Map<K, List<RawAsset<Object>>>> containedRawAssetsByClass = new HashMap<Class<? extends JsonAssetWithMap>, Map<K, List<RawAsset<Object>>>>();
        for (final T t2 : assets) {
            final AssetExtraInfo.Data data2 = this.codec.getData(t2);
            if (data2 != null) {
                data2.fetchContainedRawAssets(this.keyFunction.apply(t2), containedRawAssetsByClass);
            }
        }
        for (final Map.Entry<Class<? extends JsonAssetWithMap>, Map<K, List<RawAsset<Object>>>> entry2 : containedRawAssetsByClass.entrySet()) {
            final Class<? extends JsonAssetWithMap> assetClass2 = entry2.getKey();
            final Map<K, List<RawAsset<Object>>> containedAssets2 = entry2.getValue();
            final AssetStore assetStore2 = AssetRegistry.getAssetStore(assetClass2);
            this.loadedContainedAssetsFor.add(assetClass2);
            final List<RawAsset<?>> childList2 = new ArrayList<RawAsset<?>>();
            for (final Map.Entry<K, List<RawAsset<Object>>> containedEntry2 : containedAssets2.entrySet()) {
                final K key2 = containedEntry2.getKey();
                for (final RawAsset<Object> contained2 : containedEntry2.getValue()) {
                    final Object containedKey2 = contained2.getKey();
                    this.childAssetsMap.computeIfAbsent(assetStore2.getAssetClass(), k -> new ConcurrentHashMap()).computeIfAbsent(key2, k -> ConcurrentHashMap.newKeySet()).add(containedKey2);
                    final RawAsset<Object> resolvedContained = switch (contained2.getContainedAssetMode()) {
                        default -> throw new MatchException(null, null);
                        case NONE,  GENERATE_ID,  INJECT_PARENT,  INHERIT_ID -> contained2;
                        case INHERIT_ID_AND_PARENT -> {
                            final Object parentKey = contained2.getParentKey();
                            if (parentKey == null) {
                                yield contained2;
                            }
                            if (assetStore2.getAssetMap().getAsset(parentKey) != null || containedAssets2.containsKey(parentKey)) {
                                yield contained2;
                            }
                            this.logger.at(Level.WARNING).log("Failed to find inherited parent asset %s (%s) for %s", parentKey, assetStore2.getAssetClass().getSimpleName(), containedKey2);
                            yield contained2.withResolveKeys(containedKey2, null);
                        }
                    };
                    childList2.add(resolvedContained);
                }
            }
            final AssetLoadResult result2 = assetStore2.loadBuffersWithKeys(packKey, childList2, query, forceLoadAll);
            childAssetsResults.put(assetClass2, result2);
        }
    }
    
    protected void decodeAssets(@Nonnull final String packKey, @Nonnull final List<RawAsset<K>> rawAssets, @Nonnull final Map<K, T> loadedAssets, @Nonnull final Map<K, Path> loadedKeyToPathMap, @Nonnull final Map<K, Set<K>> loadedAssetChildren, @Nonnull final Set<K> failedToLoadKeys, @Nonnull final Set<Path> failedToLoadPaths, @Nullable final M assetMap, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll) {
        if (rawAssets.isEmpty()) {
            return;
        }
        final Map<K, RawAsset<K>> waitingForParent = new ConcurrentHashMap<K, RawAsset<K>>();
        final CompletableFuture<DecodedAsset<K, T>>[] futuresArr = new CompletableFuture[rawAssets.size()];
        for (int i = 0; i < rawAssets.size(); ++i) {
            futuresArr[i] = this.executeAssetDecode(loadedAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, assetMap, query, forceLoadAll, waitingForParent, rawAssets.get(i));
        }
        CompletableFuture.allOf((CompletableFuture<?>[])futuresArr).join();
        for (final CompletableFuture<DecodedAsset<K, T>> future : futuresArr) {
            final DecodedAsset<K, T> decodedAsset = future.getNow(null);
            if (decodedAsset != null) {
                loadedAssets.put(decodedAsset.getKey(), decodedAsset.getAsset());
            }
        }
        final List<CompletableFuture<DecodedAsset<K, T>>> futures = new ArrayList<CompletableFuture<DecodedAsset<K, T>>>();
        while (!waitingForParent.isEmpty()) {
            int processedAssets = 0;
            for (final Map.Entry<K, RawAsset<K>> entry : waitingForParent.entrySet()) {
                final K key = entry.getKey();
                final RawAsset<K> rawAsset = entry.getValue();
                final Path path = rawAsset.getPath();
                final K parentKey = rawAsset.getParentKey();
                T parent = loadedAssets.get(parentKey);
                if (parent == null) {
                    if (waitingForParent.containsKey(parentKey)) {
                        continue;
                    }
                    if (assetMap == null) {
                        this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, path);
                        this.logger.at(Level.SEVERE).log("Failed to find parent '%s' for asset: %s, %s (assetMap was null)", parentKey, key, path);
                        continue;
                    }
                    parent = (parentKey.equals("super") ? assetMap.getAsset(packKey, key) : assetMap.getAsset(parentKey));
                    if (parent == null) {
                        this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, path);
                        this.logger.at(Level.SEVERE).log("Failed to find parent '%s' for asset: %s,  %s", parentKey, key, path);
                        continue;
                    }
                }
                if (this.isUnknown.test(parent)) {
                    this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, path);
                    this.logger.at(Level.SEVERE).log("Parent '%s' for asset: %s,  %s is an unknown type", parentKey, key, path);
                }
                else {
                    ++processedAssets;
                    final T finalParent = parent;
                    futures.add(CompletableFuture.supplyAsync(() -> {
                        final char[] buffer = RawJsonReader.READ_BUFFER.get();
                        RawJsonReader reader;
                        if (rawAsset.getBuffer() != null) {
                            reader = RawJsonReader.fromBuffer(rawAsset.getBuffer());
                        }
                        else {
                            try {
                                reader = RawJsonReader.fromPath(path, buffer);
                            }
                            catch (final IOException e) {
                                this.logger.at(Level.SEVERE).withCause(e).log("Failed to load asset: %s", path);
                                return null;
                            }
                        }
                        DecodedAsset<K, T> decodedAsset3 = null;
                        try {
                            decodedAsset3 = this.decodeAssetWithParent0(loadedAssets, loadedKeyToPathMap, loadedAssetChildren, failedToLoadKeys, failedToLoadPaths, (M)assetMap, query, forceLoadAll, rawAsset, reader, (T)finalParent);
                        }
                        finally {
                            try {
                                if (rawAsset.getBuffer() != null) {
                                    reader.close();
                                }
                                else {
                                    final char[] value = reader.closeAndTakeBuffer();
                                    if (value.length > buffer.length) {
                                        RawJsonReader.READ_BUFFER.set(value);
                                    }
                                }
                            }
                            catch (final IOException e2) {
                                this.logger.at(Level.SEVERE).withCause(e2).log("Failed to close asset reader: %s", path);
                            }
                            if (decodedAsset3 == null) {
                                waitingForParent.remove(key);
                            }
                        }
                        return decodedAsset3;
                    }));
                }
            }
            final CompletableFuture<DecodedAsset<K, T>>[] futuresArray = futures.toArray(CompletableFuture[]::new);
            CompletableFuture.allOf((CompletableFuture<?>[])futuresArray).join();
            futures.clear();
            for (final CompletableFuture<DecodedAsset<K, T>> future2 : futuresArray) {
                final DecodedAsset<K, T> decodedAsset2 = future2.getNow(null);
                if (decodedAsset2 != null) {
                    loadedAssets.put(decodedAsset2.getKey(), decodedAsset2.getAsset());
                    waitingForParent.remove(decodedAsset2.getKey());
                }
            }
            if (processedAssets == 0) {
                for (final Map.Entry<K, RawAsset<K>> entry2 : waitingForParent.entrySet()) {
                    final K key2 = entry2.getKey();
                    final Path assetPath = entry2.getValue().getPath();
                    final K parentKey = entry2.getValue().getParentKey();
                    this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key2, assetPath);
                    this.logger.at(Level.SEVERE).log("Failed to find parent with key '%s' for asset: %s, %s", parentKey, key2, assetPath);
                }
                break;
            }
        }
    }
    
    @Nonnull
    private CompletableFuture<DecodedAsset<K, T>> executeAssetDecode(@Nonnull final Map<K, T> loadedAssets, @Nonnull final Map<K, Path> loadedKeyToPathMap, @Nonnull final Set<K> failedToLoadKeys, @Nonnull final Set<Path> failedToLoadPaths, final M assetMap, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll, @Nonnull final Map<K, RawAsset<K>> waitingForParent, @Nonnull final RawAsset<K> rawAsset) {
        return CompletableFuture.supplyAsync(() -> {
            RawJsonReader reader;
            try {
                final ThreadLocal<char[]> read_BUFFER = RawJsonReader.READ_BUFFER;
                Objects.requireNonNull(read_BUFFER);
                reader = rawAsset.toRawJsonReader(read_BUFFER::get);
            }
            catch (final IOException e) {
                this.logger.at(Level.SEVERE).withCause(e).log("Failed to load asset: %s", rawAsset);
                return null;
            }
            AssetHolder<K> holder;
            try {
                holder = this.decodeAsset0(loadedAssets, loadedKeyToPathMap, failedToLoadKeys, failedToLoadPaths, (M)assetMap, query, forceLoadAll, rawAsset, reader);
                if (holder instanceof final RawAsset rawAsset2) {
                    final RawAsset<K> waiting = rawAsset2;
                    waitingForParent.put(waiting.getKey(), waiting);
                }
            }
            finally {
                try {
                    if (rawAsset.getBuffer() != null) {
                        reader.close();
                    }
                    else {
                        final char[] value = reader.closeAndTakeBuffer();
                        if (value.length > RawJsonReader.READ_BUFFER.get().length) {
                            RawJsonReader.READ_BUFFER.set(value);
                        }
                    }
                }
                catch (final IOException e2) {
                    this.logger.at(Level.SEVERE).withCause(e2).log("Failed to close asset reader: %s", this.path);
                }
            }
            return (DecodedAsset<K, T>)((holder instanceof DecodedAsset) ? holder : null);
        });
    }
    
    @Nullable
    private AssetHolder<K> decodeAsset0(@Nonnull final Map<K, T> loadedAssets, @Nonnull final Map<K, Path> loadedKeyToPathMap, @Nonnull final Set<K> failedToLoadKeys, @Nonnull final Set<Path> failedToLoadPaths, @Nullable final M assetMap, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll, @Nonnull final RawAsset<K> rawAsset, @Nonnull final RawJsonReader reader) {
        Path assetPath = rawAsset.getPath();
        final long start = System.nanoTime();
        K key = rawAsset.getKey();
        K parentKey = rawAsset.getParentKey();
        try {
            final KeyedCodec<K> keyCodec = this.codec.getKeyCodec();
            final KeyedCodec<K> parentCodec = this.codec.getParentCodec();
            if (key == null) {
                if (rawAsset.getPath() != null) {
                    throw new IllegalArgumentException("Asset with path should infer its 'Id'!");
                }
                reader.mark();
                if (parentCodec != null && !rawAsset.isParentKeyResolved()) {
                    String s = RawJsonReader.seekToKeyFromObjectStart(reader, keyCodec.getKey(), parentCodec.getKey());
                    if (s != null) {
                        if (keyCodec.getKey().equals(s)) {
                            key = keyCodec.getChildCodec().decodeJson(reader);
                        }
                        else if (parentCodec.getKey().equals(s)) {
                            parentKey = parentCodec.getChildCodec().decodeJson(reader);
                        }
                        s = RawJsonReader.seekToKeyFromObjectContinued(reader, keyCodec.getKey(), parentCodec.getKey());
                        if (s != null) {
                            if (keyCodec.getKey().equals(s)) {
                                key = keyCodec.getChildCodec().decodeJson(reader);
                            }
                            else if (parentCodec.getKey().equals(s)) {
                                parentKey = parentCodec.getChildCodec().decodeJson(reader);
                            }
                        }
                    }
                }
                else if (RawJsonReader.seekToKey(reader, keyCodec.getKey())) {
                    key = keyCodec.getChildCodec().decodeJson(reader);
                }
                if (key == null) {
                    throw new CodecException("Unable to find 'Id' in document!");
                }
                reader.reset();
            }
            else if (parentCodec != null && !rawAsset.isParentKeyResolved()) {
                reader.mark();
                if (RawJsonReader.seekToKey(reader, parentCodec.getKey())) {
                    parentKey = parentCodec.getChildCodec().decodeJson(reader);
                }
                reader.reset();
            }
            if (assetPath == null) {
                assetPath = loadedKeyToPathMap.get(key);
            }
            if (parentKey != null) {
                return rawAsset.withResolveKeys(key, parentKey);
            }
            final AssetExtraInfo<K> extraInfo = new AssetExtraInfo<K>(assetPath, rawAsset.makeData(this.getAssetClass(), key, null));
            reader.consumeWhiteSpace();
            final T asset = this.codec.decodeJsonAsset(reader, extraInfo);
            if (asset == null) {
                throw new NullPointerException(rawAsset.toString());
            }
            extraInfo.getValidationResults().logOrThrowValidatorExceptions(this.logger, "Failed to validate asset!\n", (assetPath == null) ? rawAsset.getParentPath() : assetPath, rawAsset.getLineOffset());
            if (!AssetStore.DISABLE_ASSET_COMPARE && (query == null || !query.isDisableAssetCompare()) && assetMap != null && asset.equals(((AssetMap<K, Object>)assetMap).getAsset(key))) {
                this.logger.at(Level.INFO).log("Skipping asset that hasn't changed: %s", key);
                return null;
            }
            this.testKeyFormat(key, assetPath);
            if (!forceLoadAll) {}
            if (assetPath != null) {
                loadedKeyToPathMap.put(key, assetPath);
            }
            this.logUnusedKeys(key, assetPath, extraInfo);
            this.logLoadedAsset(key, null, assetPath);
            return new DecodedAsset<K, Object>(key, asset);
        }
        catch (final CodecValidationException e) {
            this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, assetPath);
            this.logger.at(Level.SEVERE).log("Failed to validate asset: %s, %s, %s", key, assetPath, e.getMessage());
        }
        catch (final IOException | CodecException e2) {
            this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, assetPath);
            if (GithubMessageUtil.isGithub()) {
                final String pathStr = (assetPath == null) ? ((key == null) ? "unknown" : key.toString()) : assetPath.toString();
                String message;
                if (e2 instanceof final CodecException codecException) {
                    message = codecException.getMessage();
                    if (codecException.getCause() != null) {
                        message = message + "\nCause: " + codecException.getCause().getMessage();
                    }
                }
                else {
                    message = e2.getMessage();
                }
                if (reader.getLine() == -1) {
                    HytaleLoggerBackend.rawLog(GithubMessageUtil.messageError(pathStr, message));
                }
                else {
                    HytaleLoggerBackend.rawLog(GithubMessageUtil.messageError(pathStr, reader.getLine(), reader.getColumn(), message));
                }
            }
            this.logger.at(Level.SEVERE).withCause(new SkipSentryException(e2)).log("Failed to decode asset: %s, %s:\n%s", key, assetPath, reader);
        }
        catch (final Throwable e3) {
            this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, assetPath);
            if (GithubMessageUtil.isGithub()) {
                final String pathStr = (assetPath == null) ? ((key == null) ? "unknown" : key.toString()) : assetPath.toString();
                final String message = e3.getMessage();
                HytaleLoggerBackend.rawLog(GithubMessageUtil.messageError(pathStr, message));
            }
            this.logger.at(Level.SEVERE).withCause(e3).log("Failed to decode asset: %s, %s", key, assetPath);
        }
        return null;
    }
    
    @Nullable
    private DecodedAsset<K, T> decodeAssetWithParent0(@Nonnull final Map<K, T> loadedAssets, @Nonnull final Map<K, Path> loadedKeyToPathMap, @Nonnull final Map<K, Set<K>> loadedAssetChildren, @Nonnull final Set<K> failedToLoadKeys, @Nonnull final Set<Path> failedToLoadPaths, @Nullable final M assetMap, @Nonnull final AssetUpdateQuery query, final boolean forceLoadAll, @Nonnull final RawAsset<K> rawAsset, @Nonnull final RawJsonReader reader, final T parent) {
        final K key = rawAsset.getKey();
        if (!rawAsset.isParentKeyResolved()) {
            throw new IllegalArgumentException("Parent key is required when decoding an asset with a parent!");
        }
        final K parentKey = rawAsset.getParentKey();
        Path assetPath = rawAsset.getPath();
        try {
            if (assetPath == null) {
                assetPath = loadedKeyToPathMap.get(key);
            }
            final AssetExtraInfo<K> extraInfo = new AssetExtraInfo<K>(assetPath, rawAsset.makeData(this.getAssetClass(), key, parentKey));
            reader.consumeWhiteSpace();
            final T asset = this.codec.decodeAndInheritJsonAsset(reader, parent, extraInfo);
            if (asset == null) {
                throw new NullPointerException(assetPath.toString());
            }
            extraInfo.getValidationResults().logOrThrowValidatorExceptions(this.logger);
            if (key.equals(parentKey)) {
                this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, assetPath);
                this.logger.at(Level.SEVERE).log("Failed to load asset '%s' because it is its own parent!", key);
                return null;
            }
            if (!AssetStore.DISABLE_ASSET_COMPARE && (query == null || !query.isDisableAssetCompare()) && assetMap != null && asset.equals(((AssetMap<K, Object>)assetMap).getAsset(key))) {
                this.logger.at(Level.INFO).log("Skipping asset that hasn't changed: %s", key);
                return null;
            }
            this.testKeyFormat(key, assetPath);
            if (!forceLoadAll) {}
            loadedAssetChildren.computeIfAbsent(parentKey, k -> ConcurrentHashMap.newKeySet()).add(key);
            if (assetPath != null) {
                loadedKeyToPathMap.put(key, assetPath);
            }
            this.logUnusedKeys(key, assetPath, extraInfo);
            this.logLoadedAsset(key, parentKey, assetPath);
            return new DecodedAsset<K, T>(key, asset);
        }
        catch (final CodecValidationException e) {
            this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, assetPath);
            this.logger.at(Level.SEVERE).log("Failed to decode asset: %s, %s, %s", key, assetPath, e.getMessage());
        }
        catch (final IOException | CodecException e2) {
            this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, assetPath);
            this.logger.at(Level.SEVERE).withCause(new SkipSentryException(e2)).log("Failed to decode asset: %s, %s:\n%s", key, assetPath, reader);
        }
        catch (final Exception e2) {
            this.recordFailedToLoad(failedToLoadKeys, failedToLoadPaths, key, assetPath);
            this.logger.at(Level.SEVERE).withCause(e2).log("Failed to decode asset: %s, %s", key, assetPath);
        }
        return null;
    }
    
    private void loadAllChildren(@Nonnull final Map<K, T> loadedAssets, @Nonnull final Collection<T> assetKeys, @Nonnull final Set<Path> documents) {
        for (final T asset : assetKeys) {
            Objects.requireNonNull(asset, "asset can't be null");
            final K key = this.keyFunction.apply(asset);
            if (key == null) {
                throw new NullPointerException(String.format("key can't be null: %s", asset));
            }
            loadedAssets.put(key, asset);
            if (!this.loadAllChildren(documents, key)) {
                continue;
            }
            final StringBuilder sb = new StringBuilder();
            sb.append(key).append(":\n");
            this.logChildTree(sb, "  ", key, new HashSet<K>());
            this.logger.at(Level.SEVERE).log("Found a circular dependency when trying to collect all children!\n%s", sb);
        }
    }
    
    protected boolean loadAllChildren(@Nonnull final Set<Path> documents, final K key) {
        final Set<K> set = ((AssetMap<K, T>)this.assetMap).getChildren(key);
        if (set == null) {
            return false;
        }
        boolean circular = false;
        for (final K child : set) {
            final Path childPath = ((AssetMap<K, T>)this.assetMap).getPath(child);
            if (childPath == null) {
                continue;
            }
            circular = (!documents.add(childPath) || (circular | this.loadAllChildren(documents, child)));
        }
        return circular;
    }
    
    protected void collectAllChildren(final K key, @Nonnull final Set<K> children) {
        if (this.collectAllChildren0(key, children)) {
            final StringBuilder sb = new StringBuilder();
            sb.append(key).append(":\n");
            this.logChildTree(sb, "  ", key, new HashSet<K>());
            this.logger.at(Level.SEVERE).log("Found a circular dependency when trying to collect all children!\n%s", sb);
        }
    }
    
    private boolean collectAllChildren0(final K key, @Nonnull final Set<K> children) {
        final Set<K> set = ((AssetMap<K, T>)this.assetMap).getChildren(key);
        if (set == null) {
            return false;
        }
        boolean circular = false;
        for (final K child : set) {
            circular = (!children.add(child) || (circular | this.collectAllChildren0(child, children)));
        }
        return circular;
    }
    
    protected void logChildTree(@Nonnull final StringBuilder sb, final String indent, final K key, @Nonnull final Set<K> children) {
        final Set<K> set = ((AssetMap<K, T>)this.assetMap).getChildren(key);
        if (set == null) {
            return;
        }
        for (K child : set) {
            if (children.add(child)) {
                sb.append(indent).append("- ").append(child).append('\n');
                this.logChildTree(sb, indent + "  ", child, children);
            }
            else {
                sb.append(indent).append("- ").append(child).append('\n').append(indent).append("  ").append("** Circular **\n");
            }
        }
    }
    
    protected void logRemoveChildren(final K parentKey, @Nonnull final Set<K> toBeRemoved) {
        final Path path = ((AssetMap<K, T>)this.assetMap).getPath(parentKey);
        for (final K child : toBeRemoved) {
            final Path childPath = ((AssetMap<K, T>)this.assetMap).getPath(child);
            if (childPath != null) {
                if (path != null) {
                    this.logger.at(Level.WARNING).log("Removing child asset '%s' of removed asset '%s'", childPath, path);
                }
                else {
                    this.logger.at(Level.WARNING).log("Removing child asset '%s' of removed asset '%s'", childPath, parentKey);
                }
            }
            else {
                this.logger.at(Level.WARNING).log("Removing child asset '%s' of removed asset '%s'", child, parentKey);
            }
        }
    }
    
    protected void logRemoveChildren(@Nonnull final Map<K, K> toBeRemoved) {
        for (final Map.Entry<K, K> entry : toBeRemoved.entrySet()) {
            final K child = entry.getKey();
            final K parentKey = entry.getValue();
            final Path childPath = ((AssetMap<K, T>)this.assetMap).getPath(child);
            if (childPath != null) {
                final Path path = ((AssetMap<K, T>)this.assetMap).getPath(parentKey);
                if (path != null) {
                    this.logger.at(Level.WARNING).log("Removing child asset '%s' of removed asset '%s'", childPath, path);
                }
                else {
                    this.logger.at(Level.WARNING).log("Removing child asset '%s' of removed asset '%s'", childPath, parentKey);
                }
            }
            else {
                this.logger.at(Level.WARNING).log("Removing child asset '%s' of removed asset '%s'", child, parentKey);
            }
        }
    }
    
    protected void testKeyFormat(@Nonnull final K key, @Nullable final Path assetPath) {
        final String keyStr = key.toString();
        if (StringUtil.isCapitalized(keyStr, '_')) {
            return;
        }
        final String expected = StringUtil.capitalize(keyStr, '_');
        if (assetPath == null) {
            this.logger.at(Level.WARNING).log("Asset key '%s' has incorrect format! Expected: '%s'", key, expected);
        }
        else {
            this.logger.at(Level.WARNING).log("Asset key '%s' for file '%s' has incorrect format! Expected: '%s'", key, assetPath, expected);
        }
    }
    
    public void logUnusedKeys(@Nonnull final K key, @Nullable final Path assetPath, @Nonnull final AssetExtraInfo<K> extraInfo) {
        final List<String> unknownKeys = extraInfo.getUnknownKeys();
        if (!unknownKeys.isEmpty()) {
            if (GithubMessageUtil.isGithub()) {
                final String pathStr = (assetPath == null) ? key.toString() : assetPath.toString();
                for (int i = 0; i < unknownKeys.size(); ++i) {
                    final String unknownKey = unknownKeys.get(i);
                    HytaleLoggerBackend.rawLog(GithubMessageUtil.messageWarning(pathStr, "Unused key: " + unknownKey));
                }
            }
            else if (assetPath != null) {
                this.logger.at(Level.WARNING).log("Unused key(s) in '%s' file %s: %s", key, assetPath, String.join(", ", unknownKeys));
            }
            else {
                this.logger.at(Level.WARNING).log("Unused key(s) in '%s': %s", key, String.join(", ", unknownKeys));
            }
        }
    }
    
    protected void logLoadedAsset(final K key, @Nullable final K parentKey, @Nullable final Path path) {
        if (path == null && parentKey == null) {
            this.logger.at(Level.FINE).log("Loaded asset: %s", key);
        }
        else if (path == null) {
            this.logger.at(Level.FINE).log("Loaded asset: '%s' with parent '%s'", key, parentKey);
        }
        else if (parentKey == null) {
            this.logger.at(Level.FINE).log("Loaded asset: '%s' from '%s'", key, path);
        }
        else {
            this.logger.at(Level.FINE).log("Loaded asset: '%s' from '%s' with parent '%s'", key, path, parentKey);
        }
    }
    
    protected void logRemoveAsset(final K key, @Nullable final Path path) {
        if (path == null) {
            this.logger.at(Level.FINE).log("Removed asset: '%s'", key);
        }
        else {
            this.logger.at(Level.FINE).log("Removed asset: '%s' from '%s'", key, path);
        }
    }
    
    private void recordFailedToLoad(@Nonnull final Set<K> failedToLoadKeys, @Nonnull final Set<Path> failedToLoadPaths, @Nullable final K key, @Nullable final Path path) {
        if (key != null) {
            failedToLoadKeys.add(key);
        }
        if (path != null) {
            failedToLoadPaths.add(path);
        }
    }
    
    @Nonnull
    @Override
    public String toString() {
        return "AssetStore{tClass=" + String.valueOf(this.tClass);
    }
    
    static {
        AssetStore.DISABLE_ASSET_COMPARE = true;
        AssetStore.DISABLE_DYNAMIC_DEPENDENCIES = false;
    }
    
    protected abstract static class Builder<K, T extends JsonAssetWithMap<K, M>, M extends AssetMap<K, T>, B extends Builder<K, T, M, B>>
    {
        @Nonnull
        protected final Class<K> kClass;
        @Nonnull
        protected final Class<T> tClass;
        protected final M assetMap;
        protected final Set<Class<? extends JsonAsset<?>>> loadsAfter;
        protected final Set<Class<? extends JsonAsset<?>>> loadsBefore;
        protected String path;
        @Nonnull
        protected String extension;
        protected AssetCodec<K, T> codec;
        protected Function<T, K> keyFunction;
        protected Function<K, T> replaceOnRemove;
        protected Predicate<T> isUnknown;
        protected boolean unmodifiable;
        protected List<T> preAddedAssets;
        protected Class<? extends JsonAsset<?>> idProvider;
        
        public Builder(final Class<K> kClass, final Class<T> tClass, final M assetMap) {
            this.loadsAfter = new HashSet<Class<? extends JsonAsset<?>>>();
            this.loadsBefore = new HashSet<Class<? extends JsonAsset<?>>>();
            this.extension = ".json";
            this.kClass = Objects.requireNonNull(kClass, "key class can't be null!");
            this.tClass = Objects.requireNonNull(tClass, "asset class can't be null!");
            this.assetMap = assetMap;
        }
        
        @Nonnull
        public B setPath(final String path) {
            this.path = Objects.requireNonNull(path, "path can't be null!");
            return (B)this;
        }
        
        @Nonnull
        public B setExtension(@Nonnull final String extension) {
            Objects.requireNonNull(extension, "extension can't be null!");
            if (extension.length() < 2 || extension.charAt(0) != '.') {
                throw new IllegalArgumentException("Extension must start with '.' and have at least one character after");
            }
            this.extension = extension;
            return (B)this;
        }
        
        @Nonnull
        public B setCodec(final AssetCodec<K, T> codec) {
            this.codec = Objects.requireNonNull(codec, "codec can't be null!");
            return (B)this;
        }
        
        @Nonnull
        public B setKeyFunction(final Function<T, K> keyFunction) {
            this.keyFunction = Objects.requireNonNull(keyFunction, "keyFunction can't be null!");
            return (B)this;
        }
        
        @Nonnull
        public B setIsUnknown(final Predicate<T> isUnknown) {
            this.isUnknown = Objects.requireNonNull(isUnknown, "isUnknown can't be null!");
            return (B)this;
        }
        
        @Nonnull
        @SafeVarargs
        public final B loadsAfter(final Class<? extends JsonAsset<?>>... clazz) {
            Collections.addAll(this.loadsAfter, clazz);
            return (B)this;
        }
        
        @Nonnull
        @SafeVarargs
        public final B loadsBefore(final Class<? extends JsonAsset<?>>... clazz) {
            Collections.addAll(this.loadsBefore, clazz);
            return (B)this;
        }
        
        @Nonnull
        public B setReplaceOnRemove(final Function<K, T> replaceOnRemove) {
            this.replaceOnRemove = Objects.requireNonNull(replaceOnRemove, "replaceOnRemove can't be null!");
            return (B)this;
        }
        
        @Nonnull
        public B unmodifiable() {
            this.unmodifiable = true;
            return (B)this;
        }
        
        @Nonnull
        public B preLoadAssets(@Nonnull final List<T> list) {
            if (this.preAddedAssets == null) {
                this.preAddedAssets = new ArrayList<T>();
            }
            this.preAddedAssets.addAll((Collection<? extends T>)list);
            return (B)this;
        }
        
        @Nonnull
        public B setIdProvider(final Class<? extends JsonAsset<?>> provider) {
            this.idProvider = provider;
            return (B)this;
        }
        
        public abstract AssetStore<K, T, M> build();
    }
}
