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

package com.hypixel.hytale.builtin.asseteditor;

import com.hypixel.hytale.builtin.asseteditor.data.ModifiedAsset;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetInfo;
import java.util.ArrayList;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorJsonAssetUpdated;
import org.bson.BsonValue;
import com.hypixel.hytale.builtin.asseteditor.util.BsonTransformationUtil;
import com.hypixel.hytale.builtin.asseteditor.event.AssetEditorAssetCreatedEvent;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorRebuildCaches;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorRequestChildrenListReply;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorFetchJsonAssetWithParentsReply;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorFetchAssetReply;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorUndoRedoReply;
import com.hypixel.hytale.protocol.packets.asseteditor.JsonUpdateType;
import com.hypixel.hytale.builtin.asseteditor.data.AssetUndoRedoInfo;
import java.nio.charset.StandardCharsets;
import com.hypixel.hytale.assetstore.AssetUpdateQuery;
import com.hypixel.hytale.builtin.asseteditor.util.AssetStoreUtil;
import com.hypixel.hytale.builtin.asseteditor.assettypehandler.JsonTypeHandler;
import com.hypixel.hytale.protocol.packets.asseteditor.JsonUpdateCommand;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorLastModifiedAssets;
import com.hypixel.hytale.builtin.asseteditor.event.AssetEditorSelectAssetEvent;
import java.time.Instant;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorExportComplete;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorExportAssetFinalize;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorExportAssetPart;
import com.hypixel.hytale.common.util.ArrayUtil;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorExportDeleteAssets;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorExportAssetInitialize;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorAsset;
import com.hypixel.hytale.protocol.packets.asseteditor.TimestampedAssetReference;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorUpdateAssetPack;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.Files;
import com.hypixel.hytale.builtin.asseteditor.util.AssetPathUtil;
import com.hypixel.hytale.common.plugin.PluginIdentifier;
import com.hypixel.hytale.server.core.util.BsonUtil;
import com.hypixel.hytale.common.plugin.AuthorInfo;
import com.hypixel.hytale.common.semver.Semver;
import com.hypixel.hytale.server.core.util.io.FileUtil;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.Options;
import com.hypixel.hytale.server.core.plugin.PluginManager;
import java.io.IOException;
import java.nio.file.LinkOption;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorPopupNotificationType;
import com.hypixel.hytale.event.IEventDispatcher;
import com.hypixel.hytale.builtin.asseteditor.event.AssetEditorClientDisconnectEvent;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetPackManifest;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorAssetPackSetup;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorCapabilities;
import com.hypixel.hytale.common.util.FormatUtil;
import com.hypixel.hytale.codec.ExtraInfo;
import com.hypixel.hytale.codec.EmptyExtraInfo;
import com.hypixel.hytale.protocol.packets.asseteditor.SchemaFile;
import com.hypixel.hytale.server.core.asset.AssetRegistryLoader;
import org.bson.BsonDocument;
import com.hypixel.hytale.codec.schema.SchemaContext;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorAssetUpdated;
import com.hypixel.hytale.common.util.PathUtil;
import java.nio.file.Path;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorFileEntry;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorAssetListUpdate;
import java.util.concurrent.ForkJoinPool;
import com.hypixel.hytale.assetstore.AssetMap;
import com.hypixel.hytale.assetstore.map.JsonAssetWithMap;
import java.util.concurrent.TimeUnit;
import com.hypixel.hytale.protocol.packets.assets.UpdateTranslations;
import com.hypixel.hytale.server.core.modules.i18n.I18nModule;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorDeleteAssetPack;
import com.hypixel.hytale.server.core.plugin.PluginState;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.Collections;
import java.util.List;
import com.hypixel.hytale.logger.HytaleLogger;
import java.util.Collection;
import com.hypixel.hytale.server.core.io.PacketHandler;
import com.hypixel.hytale.server.core.HytaleServer;
import java.util.Iterator;
import com.hypixel.hytale.server.core.modules.i18n.event.MessagesUpdated;
import com.hypixel.hytale.server.core.asset.common.events.CommonAssetMonitorEvent;
import com.hypixel.hytale.assetstore.event.AssetMonitorEvent;
import com.hypixel.hytale.assetstore.event.AssetStoreMonitorEvent;
import com.hypixel.hytale.server.core.asset.AssetPackUnregisterEvent;
import com.hypixel.hytale.server.core.asset.AssetPackRegisterEvent;
import com.hypixel.hytale.assetstore.event.RemoveAssetStoreEvent;
import com.hypixel.hytale.assetstore.event.RegisterAssetStoreEvent;
import com.hypixel.hytale.builtin.asseteditor.assettypehandler.CommonAssetTypeHandler;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorEditorType;
import com.hypixel.hytale.builtin.asseteditor.assettypehandler.AssetTypeHandler;
import com.hypixel.hytale.builtin.asseteditor.assettypehandler.AssetStoreTypeHandler;
import com.hypixel.hytale.assetstore.AssetStore;
import com.hypixel.hytale.assetstore.AssetRegistry;
import com.hypixel.hytale.server.core.io.handlers.InitialPacketHandler;
import com.hypixel.hytale.server.core.io.handlers.SubPacketHandler;
import com.hypixel.hytale.server.core.io.handlers.IPacketHandler;
import java.util.function.Function;
import com.hypixel.hytale.server.core.io.ServerManager;
import com.hypixel.hytale.server.core.asset.AssetModule;
import com.hypixel.hytale.common.plugin.PluginManifest;
import com.hypixel.hytale.builtin.asseteditor.datasource.StandardDataSource;
import java.util.logging.Level;
import com.hypixel.hytale.assetstore.AssetPack;
import java.util.HashSet;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import java.util.concurrent.ConcurrentHashMap;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.builtin.asseteditor.datasource.DataSource;
import javax.annotation.Nullable;
import java.util.concurrent.ScheduledFuture;
import com.hypixel.hytale.protocol.packets.asseteditor.AssetEditorSetupSchemas;
import javax.annotation.Nonnull;
import com.hypixel.hytale.codec.schema.config.Schema;
import java.util.Set;
import java.util.UUID;
import java.util.Map;
import java.util.concurrent.locks.StampedLock;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;

public class AssetEditorPlugin extends JavaPlugin
{
    private static AssetEditorPlugin instance;
    private final StampedLock globalEditLock;
    private final Map<UUID, Set<EditorClient>> uuidToEditorClients;
    private final Map<EditorClient, AssetPath> clientOpenAssetPathMapping;
    private final Set<EditorClient> clientsSubscribedToModifiedAssetsChanges;
    @Nonnull
    private Map<String, Schema> schemas;
    private AssetEditorSetupSchemas setupSchemasPacket;
    private final StampedLock initLock;
    private final Set<EditorClient> initQueue;
    @Nonnull
    private InitState initState;
    @Nullable
    private ScheduledFuture<?> scheduledReinitFuture;
    private final Map<String, DataSource> assetPackDataSources;
    private final AssetTypeRegistry assetTypeRegistry;
    private final UndoRedoManager undoRedoManager;
    private ScheduledFuture<?> pingClientsTask;
    
    public static AssetEditorPlugin get() {
        return AssetEditorPlugin.instance;
    }
    
    public AssetEditorPlugin(@Nonnull final JavaPluginInit init) {
        super(init);
        this.globalEditLock = new StampedLock();
        this.uuidToEditorClients = new ConcurrentHashMap<UUID, Set<EditorClient>>();
        this.clientOpenAssetPathMapping = new ConcurrentHashMap<EditorClient, AssetPath>();
        this.clientsSubscribedToModifiedAssetsChanges = (Set<EditorClient>)ConcurrentHashMap.newKeySet();
        this.schemas = new Object2ObjectOpenHashMap<String, Schema>();
        this.initLock = new StampedLock();
        this.initQueue = new HashSet<EditorClient>();
        this.initState = InitState.NOT_INITIALIZED;
        this.assetPackDataSources = new ConcurrentHashMap<String, DataSource>();
        this.assetTypeRegistry = new AssetTypeRegistry();
        this.undoRedoManager = new UndoRedoManager();
    }
    
    @Nullable
    DataSource registerDataSourceForPack(final AssetPack assetPack) {
        final PluginManifest manifest = assetPack.getManifest();
        if (manifest == null) {
            this.getLogger().at(Level.SEVERE).log("Could not load asset pack manifest for " + assetPack.getName());
            return null;
        }
        final StandardDataSource dataSource = new StandardDataSource(assetPack.getName(), assetPack.getRoot(), assetPack.isImmutable(), manifest);
        this.assetPackDataSources.put(assetPack.getName(), dataSource);
        return dataSource;
    }
    
    @Override
    protected void setup() {
        AssetEditorPlugin.instance = this;
        for (final AssetPack assetPack : AssetModule.get().getAssetPacks()) {
            this.registerDataSourceForPack(assetPack);
        }
        ServerManager.get().registerSubPacketHandlers((Function<IPacketHandler, SubPacketHandler>)AssetEditorGamePacketHandler::new);
        InitialPacketHandler.EDITOR_PACKET_HANDLER_SUPPLIER = AssetEditorPacketHandler::new;
        for (final AssetStore<?, ?, ?> assetStore : AssetRegistry.getStoreMap().values()) {
            if (assetStore.getPath() != null) {
                if (assetStore.getPath().startsWith("../")) {
                    continue;
                }
                this.assetTypeRegistry.registerAssetType(new AssetStoreTypeHandler(assetStore));
            }
        }
        this.assetTypeRegistry.registerAssetType(new CommonAssetTypeHandler("Texture", "Texture.png", ".png", AssetEditorEditorType.Texture));
        this.assetTypeRegistry.registerAssetType(new CommonAssetTypeHandler("Model", "Model.png", ".blockymodel", AssetEditorEditorType.JsonSource));
        this.assetTypeRegistry.registerAssetType(new CommonAssetTypeHandler("Animation", "Animation.png", ".blockyanim", AssetEditorEditorType.JsonSource));
        this.assetTypeRegistry.registerAssetType(new CommonAssetTypeHandler("Sound", null, ".ogg", AssetEditorEditorType.None));
        this.assetTypeRegistry.registerAssetType(new CommonAssetTypeHandler("UI", null, ".ui", AssetEditorEditorType.Text));
        this.assetTypeRegistry.registerAssetType(new CommonAssetTypeHandler("Language", null, ".lang", AssetEditorEditorType.Text));
        this.getEventRegistry().register(RegisterAssetStoreEvent.class, this::onRegisterAssetStore);
        this.getEventRegistry().register(RemoveAssetStoreEvent.class, this::onUnregisterAssetStore);
        this.getEventRegistry().register(AssetPackRegisterEvent.class, this::onRegisterAssetPack);
        this.getEventRegistry().register(AssetPackUnregisterEvent.class, this::onUnregisterAssetPack);
        this.getEventRegistry().register((Class<? super AssetMonitorEvent>)AssetStoreMonitorEvent.class, this::onAssetMonitor);
        this.getEventRegistry().register((Class<? super AssetMonitorEvent>)CommonAssetMonitorEvent.class, this::onAssetMonitor);
        this.getEventRegistry().register(MessagesUpdated.class, this::onI18nMessagesUpdated);
        AssetSpecificFunctionality.setup();
    }
    
    @Override
    protected void start() {
        for (final DataSource dataSource : this.assetPackDataSources.values()) {
            dataSource.start();
        }
        this.pingClientsTask = HytaleServer.SCHEDULED_EXECUTOR.scheduleAtFixedRate(this::sendPingPackets, 1L, 1L, PacketHandler.PingInfo.PING_FREQUENCY_UNIT);
    }
    
    @Override
    protected void shutdown() {
        InitialPacketHandler.EDITOR_PACKET_HANDLER_SUPPLIER = null;
        final String message = HytaleServer.get().isShuttingDown() ? "Server is shutting down!" : "Asset editor was disabled!";
        for (final Set<EditorClient> clients : this.uuidToEditorClients.values()) {
            for (final EditorClient client : clients) {
                client.getPacketHandler().disconnect(message);
            }
        }
        this.pingClientsTask.cancel(false);
        for (final DataSource dataSource : this.assetPackDataSources.values()) {
            dataSource.shutdown();
        }
    }
    
    public DataSource getDataSourceForPath(final AssetPath path) {
        return this.getDataSourceForPack(path.packId());
    }
    
    public DataSource getDataSourceForPack(final String assetPack) {
        return this.assetPackDataSources.get(assetPack);
    }
    
    public Collection<DataSource> getDataSources() {
        return this.assetPackDataSources.values();
    }
    
    public AssetTypeRegistry getAssetTypeRegistry() {
        return this.assetTypeRegistry;
    }
    
    public Schema getSchema(final String id) {
        return this.schemas.get(id);
    }
    
    public Map<EditorClient, AssetPath> getClientOpenAssetPathMapping() {
        return this.clientOpenAssetPathMapping;
    }
    
    public Set<EditorClient> getEditorClients(final UUID uuid) {
        return this.uuidToEditorClients.get(uuid);
    }
    
    private void sendPingPackets() {
        for (Set<EditorClient> clients : this.uuidToEditorClients.values()) {
            for (EditorClient client : clients) {
                try {
                    client.getPacketHandler().sendPing();
                }
                catch (final Exception e) {
                    this.getLogger().at(Level.SEVERE).withCause(e).log("Failed to send ping to " + String.valueOf(client));
                    client.getPacketHandler().disconnect("Exception when sending ping packet!");
                }
            }
        }
    }
    
    @Nonnull
    private List<EditorClient> getClientsWithOpenAssetPath(final AssetPath path) {
        if (this.clientOpenAssetPathMapping.isEmpty()) {
            return Collections.emptyList();
        }
        final List<EditorClient> list = new ObjectArrayList<EditorClient>();
        for (final Map.Entry<EditorClient, AssetPath> entry : this.clientOpenAssetPathMapping.entrySet()) {
            if (!entry.getValue().equals(path)) {
                continue;
            }
            list.add(entry.getKey());
        }
        return list;
    }
    
    public AssetPath getOpenAssetPath(final EditorClient editorClient) {
        return this.clientOpenAssetPathMapping.get(editorClient);
    }
    
    private void onRegisterAssetPack(final AssetPackRegisterEvent event) {
        if (this.assetPackDataSources.containsKey(event.getAssetPack().getName())) {
            return;
        }
        final DataSource dataSource = this.registerDataSourceForPack(event.getAssetPack());
        if (dataSource == null) {
            return;
        }
        if (this.getState() == PluginState.ENABLED) {
            dataSource.start();
        }
        final AssetTree tempAssetTree = dataSource.loadAssetTree(this.assetTypeRegistry.getRegisteredAssetTypeHandlers().values());
        final long globalEditStamp = this.globalEditLock.writeLock();
        try {
            dataSource.getAssetTree().replaceAssetTree(tempAssetTree);
        }
        finally {
            this.globalEditLock.unlockWrite(globalEditStamp);
        }
        this.broadcastPackAddedOrUpdated(event.getAssetPack().getName(), dataSource.getManifest());
        for (final Set<EditorClient> clients : this.uuidToEditorClients.values()) {
            for (final EditorClient client : clients) {
                dataSource.getAssetTree().sendPackets(client);
            }
        }
    }
    
    private void onUnregisterAssetPack(final AssetPackUnregisterEvent event) {
        if (!this.assetPackDataSources.containsKey(event.getAssetPack().getName())) {
            return;
        }
        final DataSource dataSource = this.assetPackDataSources.remove(event.getAssetPack().getName());
        dataSource.shutdown();
        for (final Set<EditorClient> clients : this.uuidToEditorClients.values()) {
            for (final EditorClient client : clients) {
                client.getPacketHandler().write(new AssetEditorDeleteAssetPack(event.getAssetPack().getName()));
            }
        }
    }
    
    private void onI18nMessagesUpdated(@Nonnull final MessagesUpdated event) {
        if (this.clientOpenAssetPathMapping.isEmpty()) {
            return;
        }
        final I18nModule i18nModule = I18nModule.get();
        final Map<String, Map<String, String>> changed = event.getChangedMessages();
        final Map<String, Map<String, String>> removed = event.getRemovedMessages();
        final Map<String, UpdateTranslations[]> updatePackets = new Object2ObjectOpenHashMap<String, UpdateTranslations[]>();
        for (final EditorClient client : this.clientOpenAssetPathMapping.keySet()) {
            final String languageKey = client.getLanguage();
            UpdateTranslations[] packets = updatePackets.get(languageKey);
            if (packets == null) {
                packets = i18nModule.getUpdatePacketsForChanges(languageKey, changed, removed);
                updatePackets.put(languageKey, packets);
            }
            if (packets.length != 0) {
                client.getPacketHandler().write((Packet[])packets);
            }
        }
    }
    
    private void onRegisterAssetStore(@Nonnull final RegisterAssetStoreEvent event) {
        final AssetStore<?, ? extends JsonAssetWithMap<?, ? extends AssetMap<?, ?>>, ? extends AssetMap<?, ? extends JsonAssetWithMap<?, ?>>> assetStore = (AssetStore<?, ? extends JsonAssetWithMap<?, ? extends AssetMap<?, ?>>, ? extends AssetMap<?, ? extends JsonAssetWithMap<?, ?>>>)event.getAssetStore();
        if (assetStore.getPath() == null || assetStore.getPath().startsWith("../")) {
            return;
        }
        this.assetTypeRegistry.registerAssetType(new AssetStoreTypeHandler(assetStore));
        final long stamp = this.initLock.readLock();
        try {
            if (this.initState != InitState.NOT_INITIALIZED) {
                if (this.scheduledReinitFuture != null) {
                    this.scheduledReinitFuture.cancel(false);
                }
                this.scheduledReinitFuture = HytaleServer.SCHEDULED_EXECUTOR.schedule(this::tryReinitializeAssetEditor, 1L, TimeUnit.SECONDS);
            }
        }
        finally {
            this.initLock.unlockRead(stamp);
        }
    }
    
    private void onUnregisterAssetStore(@Nonnull final RemoveAssetStoreEvent event) {
        final AssetStore<?, ? extends JsonAssetWithMap<?, ? extends AssetMap<?, ?>>, ? extends AssetMap<?, ? extends JsonAssetWithMap<?, ?>>> assetStore = (AssetStore<?, ? extends JsonAssetWithMap<?, ? extends AssetMap<?, ?>>, ? extends AssetMap<?, ? extends JsonAssetWithMap<?, ?>>>)event.getAssetStore();
        if (assetStore.getPath() == null || assetStore.getPath().startsWith("../")) {
            return;
        }
        this.assetTypeRegistry.unregisterAssetType(new AssetStoreTypeHandler(assetStore));
        final long stamp = this.initLock.readLock();
        try {
            if (this.initState != InitState.NOT_INITIALIZED) {
                if (this.scheduledReinitFuture != null) {
                    this.scheduledReinitFuture.cancel(false);
                }
                this.scheduledReinitFuture = HytaleServer.SCHEDULED_EXECUTOR.schedule(this::tryReinitializeAssetEditor, 1L, TimeUnit.SECONDS);
            }
        }
        finally {
            this.initLock.unlockRead(stamp);
        }
    }
    
    private void tryReinitializeAssetEditor() {
        final long stamp = this.initLock.writeLock();
        try {
            switch (this.initState.ordinal()) {
                case 2: {
                    this.initState = InitState.INITIALIZING;
                    this.scheduledReinitFuture = null;
                    this.getLogger().at(Level.INFO).log("Starting asset editor re-initialization");
                    ForkJoinPool.commonPool().execute(() -> this.initializeAssetEditor(true));
                    break;
                }
                case 1: {
                    this.scheduledReinitFuture = HytaleServer.SCHEDULED_EXECUTOR.schedule(this::tryReinitializeAssetEditor, 1L, TimeUnit.SECONDS);
                    break;
                }
            }
        }
        finally {
            this.initLock.unlockWrite(stamp);
        }
    }
    
    private void onAssetMonitor(@Nonnull final AssetMonitorEvent<Void> event) {
        final AssetEditorAssetListUpdate packet = new AssetEditorAssetListUpdate();
        packet.pack = event.getAssetPack();
        final ObjectArrayList<AssetEditorFileEntry> newFiles = new ObjectArrayList<AssetEditorFileEntry>();
        final DataSource dataSource = this.getDataSourceForPack(event.getAssetPack());
        if (dataSource == null) {
            return;
        }
        Path path = null;
        if (!event.getRemovedFilesAndDirectories().isEmpty()) {
            final ObjectArrayList<AssetEditorFileEntry> deletions = new ObjectArrayList<AssetEditorFileEntry>();
            final Iterator<Path> iterator = event.getRemovedFilesAndDirectories().iterator();
            while (iterator.hasNext()) {
                path = iterator.next();
                final Path relativePath = PathUtil.relativizePretty(dataSource.getRootPath(), path);
                final AssetEditorFileEntry assetFile = dataSource.getAssetTree().removeAsset(relativePath);
                if (assetFile == null) {
                    continue;
                }
                deletions.add(assetFile);
            }
            packet.deletions = deletions.toArray(AssetEditorFileEntry[]::new);
        }
        if (!event.getRemovedFilesToUnload().isEmpty()) {
            event.getRemovedFilesToUnload().removeIf(p -> {
                final Path relativePath3 = PathUtil.relativizePretty(dataSource.getRootPath(), p);
                if (!dataSource.shouldReloadAssetFromDisk(relativePath3)) {
                    this.getLogger().at(Level.INFO).log("Skipping reloading %s from file monitor event because there is changes made via the asset editor", p);
                    return true;
                }
                else {
                    final long globalEditStamp = this.globalEditLock.writeLock();
                    try {
                        this.undoRedoManager.clearUndoRedoStack(new AssetPath(event.getAssetPack(), relativePath3));
                    }
                    finally {
                        this.globalEditLock.unlockWrite(globalEditStamp);
                    }
                    return false;
                }
            });
        }
        if (!event.getCreatedOrModifiedDirectories().isEmpty()) {
            for (final Path assetFile2 : event.getCreatedOrModifiedDirectories()) {
                final Path relativePath2 = PathUtil.relativizePretty(dataSource.getRootPath(), assetFile2);
                final AssetEditorFileEntry addedAsset = dataSource.getAssetTree().ensureAsset(relativePath2, true);
                if (addedAsset == null) {
                    continue;
                }
                newFiles.add(addedAsset);
            }
        }
        if (!event.getCreatedOrModifiedFilesToLoad().isEmpty()) {
            event.getCreatedOrModifiedFilesToLoad().removeIf(path -> {
                final Path relativePath4 = PathUtil.relativizePretty(dataSource.getRootPath(), path);
                final AssetEditorFileEntry addedAsset2 = dataSource.getAssetTree().ensureAsset(relativePath4, false);
                if (addedAsset2 != null) {
                    newFiles.add(addedAsset2);
                    return false;
                }
                else if (!dataSource.shouldReloadAssetFromDisk(relativePath4)) {
                    this.getLogger().at(Level.INFO).log("Skipping reloading %s from file monitor event because there is changes made via the asset editor", path);
                    return true;
                }
                else {
                    final AssetPath assetPath = new AssetPath(event.getAssetPack(), relativePath4);
                    final long globalEditStamp2 = this.globalEditLock.writeLock();
                    try {
                        this.undoRedoManager.clearUndoRedoStack(assetPath);
                    }
                    finally {
                        this.globalEditLock.unlockWrite(globalEditStamp2);
                    }
                    final List<EditorClient> clientsWithOpenAssetPath = this.getClientsWithOpenAssetPath(assetPath);
                    if (!clientsWithOpenAssetPath.isEmpty()) {
                        final AssetEditorAssetUpdated updatePacket = new AssetEditorAssetUpdated(assetPath.toPacket(), dataSource.getAssetBytes(relativePath4));
                        for (final EditorClient editorClient : clientsWithOpenAssetPath) {
                            editorClient.getPacketHandler().write(updatePacket);
                        }
                    }
                    return false;
                }
            });
            if (!newFiles.isEmpty()) {
                packet.additions = newFiles.toArray(AssetEditorFileEntry[]::new);
            }
        }
        if (!newFiles.isEmpty()) {
            packet.additions = newFiles.toArray(AssetEditorFileEntry[]::new);
        }
        if (packet.deletions != null || packet.additions != null) {
            this.sendPacketToAllEditorUsers(packet);
        }
    }
    
    public void handleInitializeEditor(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
        assert playerRefComponent != null;
        final String username = playerRefComponent.getUsername();
        this.getLogger().at(Level.INFO).log("%s is attempting to initialize asset editor", username);
        final long stamp = this.initLock.writeLock();
        try {
            if (this.initState == InitState.NOT_INITIALIZED) {
                this.initState = InitState.INITIALIZING;
                ForkJoinPool.commonPool().execute(() -> this.initializeAssetEditor(false));
                this.getLogger().at(Level.INFO).log("%s starting asset editor initialization", username);
            }
        }
        finally {
            this.initLock.unlockWrite(stamp);
        }
    }
    
    public void handleInitializeClient(@Nonnull final EditorClient editorClient) {
        this.getLogger().at(Level.INFO).log("Initializing %s", editorClient.getUsername());
        this.uuidToEditorClients.computeIfAbsent(editorClient.getUuid(), k -> ConcurrentHashMap.newKeySet()).add(editorClient);
        this.clientOpenAssetPathMapping.put(editorClient, new AssetPath("", Path.of("", new String[0])));
        I18nModule.get().sendTranslations(editorClient.getPacketHandler(), editorClient.getLanguage());
        final long stamp = this.initLock.writeLock();
        try {
            switch (this.initState.ordinal()) {
                case 0: {
                    this.initState = InitState.INITIALIZING;
                    this.initQueue.add(editorClient);
                    ForkJoinPool.commonPool().execute(() -> this.initializeAssetEditor(false));
                    this.getLogger().at(Level.INFO).log("%s starting asset editor initialization", editorClient.getUsername());
                    return;
                }
                case 1: {
                    this.getLogger().at(Level.INFO).log("%s waiting for asset editor initialization to complete", editorClient.getUsername());
                    this.initQueue.add(editorClient);
                    return;
                }
            }
        }
        finally {
            this.initLock.unlockWrite(stamp);
        }
        this.initializeClient(editorClient);
    }
    
    private void initializeAssetEditor(final boolean updateLoadedAssets) {
        final long start = System.nanoTime();
        final Map<String, Schema> schemas = AssetRegistryLoader.generateSchemas(new SchemaContext(), new BsonDocument());
        schemas.remove("NPCRole.json");
        schemas.remove("other.json");
        final AssetEditorSetupSchemas setupSchemasPacket = new AssetEditorSetupSchemas(new SchemaFile[schemas.size()]);
        int i = 0;
        for (final Schema schema : schemas.values()) {
            final String bytes = Schema.CODEC.encode(schema, (ExtraInfo)EmptyExtraInfo.EMPTY).asDocument().toJson();
            setupSchemasPacket.schemas[i++] = new SchemaFile(bytes);
        }
        for (final DataSource dataSource : this.assetPackDataSources.values()) {
            final AssetTree tempAssetTree = dataSource.loadAssetTree(this.assetTypeRegistry.getRegisteredAssetTypeHandlers().values());
            final long globalEditStamp = this.globalEditLock.writeLock();
            try {
                dataSource.getAssetTree().replaceAssetTree(tempAssetTree);
                this.assetTypeRegistry.setupPacket();
                if (!updateLoadedAssets) {
                    continue;
                }
                dataSource.updateRuntimeAssets();
            }
            finally {
                this.globalEditLock.unlockWrite(globalEditStamp);
            }
        }
        final long globalEditStamp2 = this.globalEditLock.writeLock();
        try {
            this.schemas = schemas;
            this.setupSchemasPacket = setupSchemasPacket;
            this.assetTypeRegistry.setupPacket();
        }
        finally {
            this.globalEditLock.unlockWrite(globalEditStamp2);
        }
        final long initStamp = this.initLock.writeLock();
        try {
            this.initState = InitState.INITIALIZED;
            this.getLogger().at(Level.INFO).log("Asset editor initialization complete! Took: %s", FormatUtil.nanosToString(System.nanoTime() - start));
            for (final EditorClient editorClient : this.clientOpenAssetPathMapping.keySet()) {
                this.initializeClient(editorClient);
            }
            this.initQueue.clear();
        }
        finally {
            this.initLock.unlockWrite(initStamp);
        }
    }
    
    private void initializeClient(@Nonnull final EditorClient editorClient) {
        final DataSource defaultDataSource = this.assetPackDataSources.get("Hytale:Hytale");
        final boolean canDiscard = false;
        final boolean canEditAssets = editorClient.hasPermission("hytale.editor.asset");
        final boolean canEditAssetPacks = editorClient.hasPermission("hytale.editor.packs.edit");
        final boolean canCreateAssetPacks = editorClient.hasPermission("hytale.editor.packs.create");
        final boolean canDeleteAssetPacks = editorClient.hasPermission("hytale.editor.packs.delete");
        editorClient.getPacketHandler().write(new AssetEditorCapabilities(false, canEditAssets, canCreateAssetPacks, canEditAssetPacks, canDeleteAssetPacks));
        editorClient.getPacketHandler().write(this.setupSchemasPacket);
        this.assetTypeRegistry.sendPacket(editorClient);
        final AssetEditorAssetPackSetup packSetupPacket = new AssetEditorAssetPackSetup();
        packSetupPacket.packs = new Object2ObjectOpenHashMap<String, AssetPackManifest>();
        for (final Map.Entry<String, DataSource> dataSourceEntry : this.assetPackDataSources.entrySet()) {
            final DataSource dataSource = dataSourceEntry.getValue();
            final PluginManifest manifest = dataSource.getManifest();
            packSetupPacket.packs.put(dataSourceEntry.getKey(), toManifestPacket(manifest));
        }
        editorClient.getPacketHandler().write(packSetupPacket);
        for (final DataSource dataSource2 : this.assetPackDataSources.values()) {
            dataSource2.getAssetTree().sendPackets(editorClient);
        }
        this.getLogger().at(Level.INFO).log("Done Initializing %s", editorClient.getUsername());
    }
    
    public void handleEditorClientDisconnected(@Nonnull final EditorClient editorClient, final PacketHandler.DisconnectReason disconnectReason) {
        final IEventDispatcher<AssetEditorClientDisconnectEvent, AssetEditorClientDisconnectEvent> dispatch = HytaleServer.get().getEventBus().dispatchFor((Class<? super AssetEditorClientDisconnectEvent>)AssetEditorClientDisconnectEvent.class);
        if (dispatch.hasListener()) {
            dispatch.dispatch(new AssetEditorClientDisconnectEvent(editorClient, disconnectReason));
        }
        this.uuidToEditorClients.compute(editorClient.getUuid(), (uuid, clients) -> {
            if (clients == null) {
                return null;
            }
            else {
                clients.remove(editorClient);
                return (Set<EditorClient>)(Set<EditorClient>)(clients.isEmpty() ? null : clients);
            }
        });
        this.clientOpenAssetPathMapping.remove(editorClient);
        this.clientsSubscribedToModifiedAssetsChanges.remove(editorClient);
    }
    
    public void handleDeleteAssetPack(@Nonnull final EditorClient editorClient, @Nonnull final String packId) {
        if (packId.equalsIgnoreCase("Hytale:Hytale")) {
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        final DataSource dataSource = this.getDataSourceForPack(packId);
        if (dataSource == null) {
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        AssetModule.get().unregisterPack(packId);
        Path targetPath;
        try {
            targetPath = dataSource.getRootPath().toRealPath(new LinkOption[0]);
        }
        catch (final IOException e) {
            throw new RuntimeException("Failed to resolve the real path for asset pack directory while deleting asset pack '" + packId + "'.", (Throwable)e);
        }
        boolean isInModsDirectory = false;
        try {
            if (targetPath.startsWith(PluginManager.MODS_PATH.toRealPath(new LinkOption[0]))) {
                isInModsDirectory = true;
            }
        }
        catch (final IOException ex) {}
        if (!isInModsDirectory) {
            for (final Path modsPath : Options.getOptionSet().valuesOf(Options.MODS_DIRECTORIES)) {
                try {
                    if (targetPath.startsWith(modsPath.toRealPath(new LinkOption[0]))) {
                        isInModsDirectory = true;
                        break;
                    }
                    continue;
                }
                catch (final IOException ex2) {}
            }
        }
        if (!isInModsDirectory) {
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Message.translation("server.assetEditor.messages.packOutsideDirectory"));
            return;
        }
        try {
            FileUtil.deleteDirectory(targetPath);
        }
        catch (final Exception e2) {
            this.getLogger().at(Level.SEVERE).withCause(e2).log("Failed to delete asset pack %s from disk", packId);
        }
    }
    
    public void handleUpdateAssetPack(@Nonnull final EditorClient editorClient, @Nonnull final String packId, @Nonnull final AssetPackManifest packetManifest) {
        if (packId.equals("Hytale:Hytale")) {
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        final DataSource dataSource = this.getDataSourceForPack(packId);
        if (dataSource == null) {
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (dataSource.isImmutable()) {
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        final PluginManifest manifest = dataSource.getManifest();
        if (manifest == null) {
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Message.translation("server.assetEditor.messages.manifestNotFound"));
            return;
        }
        boolean didIdentifierChange = false;
        if (packetManifest.name != null && !packetManifest.name.isEmpty() && !manifest.getName().equals(packetManifest.name)) {
            manifest.setName(packetManifest.name);
            didIdentifierChange = true;
        }
        if (packetManifest.group != null && !packetManifest.group.isEmpty() && !manifest.getGroup().equals(packetManifest.group)) {
            manifest.setGroup(packetManifest.group);
            didIdentifierChange = true;
        }
        if (packetManifest.description != null) {
            manifest.setDescription(packetManifest.description);
        }
        if (packetManifest.website != null) {
            manifest.setWebsite(packetManifest.website);
        }
        if (packetManifest.version != null && !packetManifest.version.isEmpty()) {
            try {
                manifest.setVersion(Semver.fromString(packetManifest.version));
            }
            catch (final IllegalArgumentException e) {
                this.getLogger().at(Level.WARNING).withCause(e).log("Invalid version format: %s", packetManifest.version);
                editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Message.translation("server.assetEditor.messages.invalidVersionFormat"));
                return;
            }
        }
        if (packetManifest.authors != null) {
            final List<AuthorInfo> authors = new ObjectArrayList<AuthorInfo>();
            for (final com.hypixel.hytale.protocol.packets.asseteditor.AuthorInfo packetAuthor : packetManifest.authors) {
                final AuthorInfo author = new AuthorInfo();
                author.setName(packetAuthor.name);
                author.setEmail(packetAuthor.email);
                author.setUrl(packetAuthor.url);
                authors.add(author);
            }
            manifest.setAuthors(authors);
        }
        final Path manifestPath = dataSource.getRootPath().resolve("manifest.json");
        try {
            BsonUtil.writeSync(manifestPath, PluginManifest.CODEC, manifest, this.getLogger());
            this.getLogger().at(Level.INFO).log("Saved manifest for pack %s", packId);
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Success, Message.translation("server.assetEditor.messages.manifestSaved"));
        }
        catch (final IOException e2) {
            this.getLogger().at(Level.SEVERE).withCause(e2).log("Failed to save manifest for pack %s", packId);
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Message.translation("server.assetEditor.messages.manifestSaveFailed"));
        }
        this.broadcastPackAddedOrUpdated(packId, manifest);
        if (didIdentifierChange) {
            final String newPackId = new PluginIdentifier(manifest).toString();
            final Path packPath = dataSource.getRootPath();
            final AssetModule assetModule = AssetModule.get();
            assetModule.unregisterPack(packId);
            assetModule.registerPack(newPackId, packPath, manifest);
        }
    }
    
    public void handleCreateAssetPack(@Nonnull final EditorClient editorClient, @Nonnull final AssetPackManifest packetManifest, final int requestToken) {
        if (packetManifest.name == null || packetManifest.name.isEmpty()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.packNameRequired"));
            return;
        }
        if (packetManifest.group == null || packetManifest.group.isEmpty()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.packGroupRequired"));
            return;
        }
        final PluginManifest manifest = new PluginManifest();
        manifest.setName(packetManifest.name);
        manifest.setGroup(packetManifest.group);
        if (packetManifest.description != null) {
            manifest.setDescription(packetManifest.description);
        }
        if (packetManifest.website != null) {
            manifest.setWebsite(packetManifest.website);
        }
        if (packetManifest.version != null && !packetManifest.version.isEmpty()) {
            try {
                manifest.setVersion(Semver.fromString(packetManifest.version));
            }
            catch (final IllegalArgumentException e) {
                this.getLogger().at(Level.WARNING).withCause(e).log("Invalid version format: %s", packetManifest.version);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.invalidVersionFormat"));
                return;
            }
        }
        if (packetManifest.authors != null) {
            final List<AuthorInfo> authors = new ObjectArrayList<AuthorInfo>();
            for (final com.hypixel.hytale.protocol.packets.asseteditor.AuthorInfo packetAuthor : packetManifest.authors) {
                final AuthorInfo author = new AuthorInfo();
                author.setName(packetAuthor.name);
                author.setEmail(packetAuthor.email);
                author.setUrl(packetAuthor.url);
                authors.add(author);
            }
            manifest.setAuthors(authors);
        }
        final String packId = new PluginIdentifier(manifest).toString();
        if (this.assetPackDataSources.containsKey(packId)) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.packAlreadyExists"));
            return;
        }
        final Path modsPath = PluginManager.MODS_PATH;
        final String dirName = AssetPathUtil.removeInvalidFileNameChars((packetManifest.group != null) ? (packetManifest.group + "." + packetManifest.name) : packetManifest.name);
        final Path normalized = Path.of(dirName, new String[0]).normalize();
        if (AssetPathUtil.isInvalidFileName(normalized)) {
            editorClient.sendFailureReply(requestToken, Messages.INVALID_FILENAME_MESSAGE);
            return;
        }
        final Path packPath = modsPath.resolve(normalized).normalize();
        if (!packPath.startsWith(modsPath)) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.packOutsideDirectory"));
            return;
        }
        if (Files.exists(packPath, new LinkOption[0])) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.packAlreadyExistsAtPath"));
            return;
        }
        try {
            Files.createDirectories(packPath, (FileAttribute<?>[])new FileAttribute[0]);
            final Path manifestPath = packPath.resolve("manifest.json");
            BsonUtil.writeSync(manifestPath, PluginManifest.CODEC, manifest, this.getLogger());
            AssetModule.get().registerPack(packId, packPath, manifest);
            editorClient.sendSuccessReply(requestToken, Message.translation("server.assetEditor.messages.packCreated"));
            this.getLogger().at(Level.INFO).log("Created new pack: %s at %s", packId, packPath);
        }
        catch (final IOException e2) {
            this.getLogger().at(Level.SEVERE).withCause(e2).log("Failed to create pack %s", packId);
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.packCreationFailed"));
        }
    }
    
    private static AssetPackManifest toManifestPacket(@Nonnull final PluginManifest manifest) {
        final AssetPackManifest packet = new AssetPackManifest();
        packet.name = manifest.getName();
        packet.description = ((manifest.getDescription() != null) ? manifest.getDescription() : "");
        packet.group = manifest.getGroup();
        packet.version = ((manifest.getVersion() != null) ? manifest.getVersion().toString() : "");
        packet.website = ((manifest.getWebsite() != null) ? manifest.getWebsite() : "");
        final List<com.hypixel.hytale.protocol.packets.asseteditor.AuthorInfo> authors = new ObjectArrayList<com.hypixel.hytale.protocol.packets.asseteditor.AuthorInfo>();
        for (final AuthorInfo a : manifest.getAuthors()) {
            final com.hypixel.hytale.protocol.packets.asseteditor.AuthorInfo authorInfo = new com.hypixel.hytale.protocol.packets.asseteditor.AuthorInfo(a.getName(), a.getEmail(), a.getUrl());
            authors.add(authorInfo);
        }
        packet.authors = authors.toArray(new com.hypixel.hytale.protocol.packets.asseteditor.AuthorInfo[0]);
        return packet;
    }
    
    private void broadcastPackAddedOrUpdated(final String packId, final PluginManifest manifest) {
        final AssetPackManifest manifestPacket = toManifestPacket(manifest);
        for (final Set<EditorClient> clients : this.uuidToEditorClients.values()) {
            for (final EditorClient client : clients) {
                client.getPacketHandler().write(new AssetEditorUpdateAssetPack(packId, manifestPacket));
            }
        }
    }
    
    public void handleExportAssets(@Nonnull final EditorClient editorClient, @Nonnull final List<AssetPath> paths) {
        final ObjectArrayList<TimestampedAssetReference> exportedAssets = new ObjectArrayList<TimestampedAssetReference>();
        for (final AssetPath assetPath : paths) {
            final DataSource dataSource = this.getDataSourceForPath(assetPath);
            if (dataSource == null) {
                this.getLogger().at(Level.WARNING).log("%s has no valid data source", assetPath);
                final AssetEditorAsset asset = new AssetEditorAsset(null, assetPath.toPacket());
                editorClient.getPacketHandler().write(new AssetEditorExportAssetInitialize(asset, null, 0, true));
            }
            else if (!this.isValidPath(dataSource, assetPath)) {
                this.getLogger().at(Level.WARNING).log("%s is an invalid path", assetPath);
                final AssetEditorAsset asset = new AssetEditorAsset(null, assetPath.toPacket());
                editorClient.getPacketHandler().write(new AssetEditorExportAssetInitialize(asset, null, 0, true));
            }
            else if (this.assetTypeRegistry.getAssetTypeHandlerForPath(assetPath.path()) == null) {
                this.getLogger().at(Level.WARNING).log("%s is not a valid asset type", assetPath);
                final AssetEditorAsset asset = new AssetEditorAsset(null, assetPath.toPacket());
                editorClient.getPacketHandler().write(new AssetEditorExportAssetInitialize(asset, null, 0, true));
            }
            else if (!dataSource.doesAssetExist(assetPath.path())) {
                editorClient.getPacketHandler().write(new AssetEditorExportDeleteAssets(new AssetEditorAsset[] { new AssetEditorAsset(null, assetPath.toPacket()) }));
            }
            else {
                final byte[] bytes = dataSource.getAssetBytes(assetPath.path());
                if (bytes == null) {
                    this.getLogger().at(Level.WARNING).log("Tried to load %s for export but failed", assetPath);
                    editorClient.getPacketHandler().write(new AssetEditorExportAssetInitialize(new AssetEditorAsset(null, assetPath.toPacket()), null, 0, false));
                }
                else {
                    final byte[][] parts = ArrayUtil.split(bytes, 2621440);
                    final Packet[] packets = new Packet[2 + parts.length];
                    packets[0] = new AssetEditorExportAssetInitialize(new AssetEditorAsset(null, assetPath.toPacket()), null, bytes.length, false);
                    for (int partIndex = 0; partIndex < parts.length; ++partIndex) {
                        packets[1 + partIndex] = new AssetEditorExportAssetPart(parts[partIndex]);
                    }
                    packets[packets.length - 1] = new AssetEditorExportAssetFinalize();
                    editorClient.getPacketHandler().write(packets);
                    final Instant timestamp = dataSource.getLastModificationTimestamp(assetPath.path());
                    exportedAssets.add(new TimestampedAssetReference(assetPath.toPacket(), (timestamp != null) ? timestamp.toString() : null));
                }
            }
        }
        editorClient.getPacketHandler().write(new AssetEditorExportComplete(exportedAssets.toArray(TimestampedAssetReference[]::new)));
    }
    
    public void handleSelectAsset(@Nonnull final EditorClient editorClient, @Nullable final AssetPath assetPath) {
        if (assetPath != null) {
            final DataSource dataSource = this.getDataSourceForPath(assetPath);
            if (dataSource == null) {
                return;
            }
        }
        String assetType = null;
        String currentAssetType = null;
        final AssetPath currentPath = this.clientOpenAssetPathMapping.get(editorClient);
        if (currentPath != null && !currentPath.equals(AssetPath.EMPTY_PATH)) {
            final AssetTypeHandler currentAssetTypeHandler = this.assetTypeRegistry.tryGetAssetTypeHandler(currentPath.path(), editorClient, -1);
            if (currentAssetTypeHandler != null) {
                currentAssetType = currentAssetTypeHandler.getConfig().id;
            }
        }
        if (assetPath != null) {
            final AssetTypeHandler assetTypeHandler = this.assetTypeRegistry.tryGetAssetTypeHandler(assetPath.path(), editorClient, -1);
            if (assetTypeHandler == null) {
                return;
            }
            assetType = assetTypeHandler.getConfig().id;
            this.clientOpenAssetPathMapping.put(editorClient, assetPath);
        }
        else {
            this.clientOpenAssetPathMapping.put(editorClient, AssetPath.EMPTY_PATH);
        }
        final IEventDispatcher<AssetEditorSelectAssetEvent, AssetEditorSelectAssetEvent> dispatch = HytaleServer.get().getEventBus().dispatchFor((Class<? super AssetEditorSelectAssetEvent>)AssetEditorSelectAssetEvent.class);
        if (dispatch.hasListener()) {
            dispatch.dispatch(new AssetEditorSelectAssetEvent(editorClient, assetType, assetPath, currentAssetType, currentPath));
        }
    }
    
    public void handleFetchLastModifiedAssets(@Nonnull final EditorClient editorClient) {
        final long stamp = this.globalEditLock.readLock();
        try {
            final AssetEditorLastModifiedAssets packet = this.buildAssetEditorLastModifiedAssetsPacket();
            editorClient.getPacketHandler().write(packet);
        }
        finally {
            this.globalEditLock.unlockRead(stamp);
        }
    }
    
    public void handleAssetUpdate(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath, @Nonnull final byte[] data, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource == null) {
            editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (dataSource.isImmutable()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        final AssetTypeHandler assetTypeHandler = this.assetTypeRegistry.tryGetAssetTypeHandler(assetPath.path(), editorClient, requestToken);
        if (assetTypeHandler == null) {
            return;
        }
        final long stamp = this.globalEditLock.writeLock();
        try {
            if (!dataSource.doesAssetExist(assetPath.path())) {
                this.getLogger().at(Level.WARNING).log("%s does not exist", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.update.doesntExist"));
                return;
            }
            if (!assetTypeHandler.isValidData(data)) {
                this.getLogger().at(Level.WARNING).log("Failed to validate data for %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.createAsset.failed"));
                return;
            }
            if (!dataSource.updateAsset(assetPath.path(), data, editorClient)) {
                this.getLogger().at(Level.WARNING).log("Failed to update asset %s in data source!", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.update.failed"));
                return;
            }
            this.updateAssetForConnectedClients(assetPath, data, editorClient);
            this.sendModifiedAssetsUpdateToConnectedUsers();
            editorClient.sendSuccessReply(requestToken);
            assetTypeHandler.loadAsset(assetPath, dataSource.getFullPathToAssetData(assetPath.path()), data, editorClient);
        }
        finally {
            this.globalEditLock.unlockWrite(stamp);
        }
        this.getLogger().at(Level.INFO).log("Updated asset at %s", assetPath);
    }
    
    public void handleJsonAssetUpdate(@Nonnull final EditorClient editorClient, AssetPath assetPath, @Nonnull final String assetType, final int assetIndex, @Nonnull final JsonUpdateCommand[] commands, final int requestToken) {
        final AssetTypeHandler assetTypeHandler = this.assetTypeRegistry.getAssetTypeHandler(assetType);
        if (!(assetTypeHandler instanceof JsonTypeHandler)) {
            this.getLogger().at(Level.WARNING).log("Invalid asset type %s", assetType);
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.unknownAssetType").param("assetType", assetType));
            return;
        }
        DataSource dataSource;
        if (assetIndex > -1 && assetTypeHandler instanceof AssetStoreTypeHandler) {
            final AssetStore assetStore = ((AssetStoreTypeHandler)assetTypeHandler).getAssetStore();
            final AssetMap assetMap = assetStore.getAssetMap();
            final String keyString = AssetStoreUtil.getIdFromIndex((AssetStore<Object, JsonAssetWithMap, AssetMap>)assetStore, assetIndex);
            final Object key = assetStore.decodeStringKey(keyString);
            final Path storedPath = assetMap.getPath(key);
            final String storedAssetPack = assetMap.getAssetPack(key);
            if (storedPath == null || storedAssetPack == null) {
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.unknownAssetIndex"));
                return;
            }
            dataSource = this.getDataSourceForPack(storedAssetPack);
            if (dataSource == null) {
                editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
                return;
            }
            assetPath = new AssetPath(storedAssetPack, PathUtil.relativizePretty(dataSource.getRootPath(), storedPath));
        }
        else {
            dataSource = this.getDataSourceForPath(assetPath);
            if (dataSource == null) {
                editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
                return;
            }
        }
        if (dataSource.isImmutable()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        if (!assetPath.path().startsWith(assetTypeHandler.getRootPath())) {
            this.getLogger().at(Level.WARNING).log("%s is not within valid asset directory", assetPath);
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.directoryOutsideRoot"));
            return;
        }
        final String fileExtension = PathUtil.getFileExtension(assetPath.path());
        if (!fileExtension.equalsIgnoreCase(assetTypeHandler.getConfig().fileExtension)) {
            this.getLogger().at(Level.WARNING).log("File extension not matching. Expected %s, got %s", assetTypeHandler.getConfig().fileExtension, fileExtension);
            this.getLogger().at(Level.WARNING).log("File extension not matching. Expected %s, got %s", assetTypeHandler.getConfig().fileExtension, fileExtension);
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.fileExtensionMismatch").param("fileExtension", assetTypeHandler.getConfig().fileExtension));
            return;
        }
        final long stamp = this.globalEditLock.writeLock();
        try {
            byte[] bytes = dataSource.getAssetBytes(assetPath.path());
            if (bytes == null) {
                this.getLogger().at(Level.WARNING).log("%s does not exist", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.update.doesntExist"));
                return;
            }
            final AssetUpdateQuery.RebuildCacheBuilder rebuildCacheBuilder = AssetUpdateQuery.RebuildCache.builder();
            BsonDocument asset;
            try {
                asset = this.applyCommandsToAsset(bytes, assetPath, commands, rebuildCacheBuilder);
                final String json = BsonUtil.toJson(asset);
                bytes = json.getBytes(StandardCharsets.UTF_8);
            }
            catch (final Exception e) {
                this.getLogger().at(Level.WARNING).withCause(e).log("Failed to apply commands to %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.update.failed"));
                return;
            }
            if (!dataSource.updateAsset(assetPath.path(), bytes, editorClient)) {
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.update.failed"));
                return;
            }
            final AssetUndoRedoInfo undoRedo = this.undoRedoManager.getOrCreateUndoRedoStack(assetPath);
            undoRedo.redoStack.clear();
            for (final JsonUpdateCommand command : commands) {
                undoRedo.undoStack.push(command);
            }
            this.updateJsonAssetForConnectedClients(assetPath, commands, editorClient);
            editorClient.sendSuccessReply(requestToken);
            this.sendModifiedAssetsUpdateToConnectedUsers();
            ((JsonTypeHandler)assetTypeHandler).loadAssetFromDocument(assetPath, dataSource.getFullPathToAssetData(assetPath.path()), asset.clone(), new AssetUpdateQuery(rebuildCacheBuilder.build()), editorClient);
        }
        finally {
            this.globalEditLock.unlockWrite(stamp);
        }
    }
    
    public void handleUndo(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource == null) {
            editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (dataSource.isImmutable()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        final AssetTypeHandler assetTypeHandler = this.assetTypeRegistry.tryGetAssetTypeHandler(assetPath.path(), editorClient, requestToken);
        if (assetTypeHandler == null) {
            return;
        }
        if (!(assetTypeHandler instanceof JsonTypeHandler)) {
            this.getLogger().at(Level.WARNING).log("Undo can only be applied to an instance of JsonTypeHandler");
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.invalidAssetType"));
            return;
        }
        final long stamp = this.globalEditLock.writeLock();
        try {
            final AssetUndoRedoInfo undoRedo = this.undoRedoManager.getUndoRedoStack(assetPath);
            if (undoRedo == null || undoRedo.undoStack.isEmpty()) {
                this.getLogger().at(Level.INFO).log("Nothing to undo");
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.undo.empty"));
                return;
            }
            final JsonUpdateCommand command = undoRedo.undoStack.peek();
            final JsonUpdateCommand undoCommand = new JsonUpdateCommand();
            undoCommand.rebuildCaches = command.rebuildCaches;
            if (command.firstCreatedProperty != null) {
                undoCommand.type = JsonUpdateType.RemoveProperty;
                undoCommand.path = command.firstCreatedProperty;
            }
            else {
                undoCommand.type = ((command.type == JsonUpdateType.RemoveProperty) ? JsonUpdateType.InsertProperty : JsonUpdateType.SetProperty);
                undoCommand.path = command.path;
                undoCommand.value = command.previousValue;
            }
            byte[] bytes = dataSource.getAssetBytes(assetPath.path());
            if (bytes == null) {
                this.getLogger().at(Level.WARNING).log("%s does not exist", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.update.doesntExist"));
                return;
            }
            final AssetUpdateQuery.RebuildCacheBuilder rebuildCacheBuilder = AssetUpdateQuery.RebuildCache.builder();
            BsonDocument asset;
            try {
                asset = this.applyCommandsToAsset(bytes, assetPath, new JsonUpdateCommand[] { undoCommand }, rebuildCacheBuilder);
                final String json = BsonUtil.toJson(asset);
                bytes = json.getBytes(StandardCharsets.UTF_8);
            }
            catch (final Exception e) {
                this.getLogger().at(Level.WARNING).withCause(e).log("Failed to undo for %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.undo.failed"));
                return;
            }
            if (!dataSource.updateAsset(assetPath.path(), bytes, editorClient)) {
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.undo.failed"));
                return;
            }
            undoRedo.undoStack.poll();
            undoRedo.redoStack.push(command);
            this.updateJsonAssetForConnectedClients(assetPath, new JsonUpdateCommand[] { undoCommand }, editorClient);
            editorClient.getPacketHandler().write(new AssetEditorUndoRedoReply(requestToken, undoCommand));
            this.sendModifiedAssetsUpdateToConnectedUsers();
            ((JsonTypeHandler)assetTypeHandler).loadAssetFromDocument(assetPath, dataSource.getFullPathToAssetData(assetPath.path()), asset.clone(), new AssetUpdateQuery(rebuildCacheBuilder.build()), editorClient);
        }
        finally {
            this.globalEditLock.unlockWrite(stamp);
        }
    }
    
    public void handleRedo(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource == null) {
            editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (dataSource.isImmutable()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        final AssetTypeHandler assetTypeHandler = this.assetTypeRegistry.tryGetAssetTypeHandler(assetPath.path(), editorClient, requestToken);
        if (assetTypeHandler == null) {
            return;
        }
        if (!(assetTypeHandler instanceof JsonTypeHandler)) {
            this.getLogger().at(Level.WARNING).log("Redo can only be applied to an instance of JsonTypeHandler");
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.invalidAssetType"));
            return;
        }
        final long stamp = this.globalEditLock.writeLock();
        try {
            final AssetUndoRedoInfo undoRedo = this.undoRedoManager.getUndoRedoStack(assetPath);
            if (undoRedo == null || undoRedo.redoStack.isEmpty()) {
                this.getLogger().at(Level.WARNING).log("Nothing to redo");
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.redo.empty"));
                return;
            }
            byte[] bytes = dataSource.getAssetBytes(assetPath.path());
            if (bytes == null) {
                this.getLogger().at(Level.WARNING).log("%s does not exist", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.update.doesntExist"));
                return;
            }
            final JsonUpdateCommand command = undoRedo.redoStack.peek();
            final AssetUpdateQuery.RebuildCacheBuilder rebuildCacheBuilder = AssetUpdateQuery.RebuildCache.builder();
            BsonDocument asset;
            try {
                asset = this.applyCommandsToAsset(bytes, assetPath, new JsonUpdateCommand[] { command }, rebuildCacheBuilder);
                final String json = BsonUtil.toJson(asset);
                bytes = json.getBytes(StandardCharsets.UTF_8);
            }
            catch (final Exception e) {
                this.getLogger().at(Level.WARNING).withCause(e).log("Failed to redo for %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.redo.failed"));
                return;
            }
            if (!dataSource.updateAsset(assetPath.path(), bytes, editorClient)) {
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.redo.failed"));
                return;
            }
            undoRedo.redoStack.poll();
            undoRedo.undoStack.push(command);
            this.updateJsonAssetForConnectedClients(assetPath, new JsonUpdateCommand[] { command }, editorClient);
            editorClient.getPacketHandler().write(new AssetEditorUndoRedoReply(requestToken, command));
            this.sendModifiedAssetsUpdateToConnectedUsers();
            ((JsonTypeHandler)assetTypeHandler).loadAssetFromDocument(assetPath, dataSource.getFullPathToAssetData(assetPath.path()), asset.clone(), new AssetUpdateQuery(rebuildCacheBuilder.build()), editorClient);
        }
        finally {
            this.globalEditLock.unlockWrite(stamp);
        }
    }
    
    public void handleFetchAsset(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource == null) {
            editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        if (this.assetTypeRegistry.tryGetAssetTypeHandler(assetPath.path(), editorClient, requestToken) == null) {
            return;
        }
        final long stamp = this.globalEditLock.readLock();
        try {
            if (!dataSource.doesAssetExist(assetPath.path())) {
                this.getLogger().at(Level.WARNING).log("%s is not a regular file", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.fetchAsset.doesntExist"));
                return;
            }
            final byte[] asset = dataSource.getAssetBytes(assetPath.path());
            if (asset == null) {
                this.getLogger().at(Level.INFO).log("Failed to get '%s'", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.fetchAsset.failed"));
                return;
            }
            this.getLogger().at(Level.INFO).log("Got '%s'", assetPath);
            editorClient.getPacketHandler().write(new AssetEditorFetchAssetReply(requestToken, asset));
        }
        finally {
            this.globalEditLock.unlockRead(stamp);
        }
    }
    
    public void handleFetchJsonAssetWithParents(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath, final boolean isFromOpenedTab, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource == null) {
            editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        if (this.assetTypeRegistry.tryGetAssetTypeHandler(assetPath.path(), editorClient, requestToken) == null) {
            return;
        }
        final long stamp = this.globalEditLock.readLock();
        try {
            final byte[] asset = dataSource.getAssetBytes(assetPath.path());
            if (asset == null) {
                this.getLogger().at(Level.INFO).log("Failed to get '%s'", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.fetchAsset.failed"));
                return;
            }
            this.getLogger().at(Level.INFO).log("Got '%s'", assetPath);
            final BsonDocument bson = BsonDocument.parse(new String(asset, StandardCharsets.UTF_8));
            final Object2ObjectOpenHashMap<com.hypixel.hytale.protocol.packets.asseteditor.AssetPath, String> assets = new Object2ObjectOpenHashMap<com.hypixel.hytale.protocol.packets.asseteditor.AssetPath, String>();
            assets.put(assetPath.toPacket(), BsonUtil.translateBsonToJson(bson).getAsJsonObject().toString());
            editorClient.getPacketHandler().write(new AssetEditorFetchJsonAssetWithParentsReply(requestToken, assets));
        }
        finally {
            this.globalEditLock.unlockRead(stamp);
        }
    }
    
    public void handleRequestChildIds(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource == null) {
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        final AssetTypeHandler assetTypeHandler = this.assetTypeRegistry.getAssetTypeHandlerForPath(assetPath.path());
        if (!(assetTypeHandler instanceof AssetStoreTypeHandler)) {
            this.getLogger().at(Level.WARNING).log("Invalid asset type for %s", assetPath);
            editorClient.sendPopupNotification(AssetEditorPopupNotificationType.Error, Message.translation("server.assetEditor.messages.requestChildIds.assetTypeMissing"));
            return;
        }
        final AssetStore assetStore = ((AssetStoreTypeHandler)assetTypeHandler).getAssetStore();
        final Object key = assetStore.decodeFilePathKey(assetPath.path());
        final Set children = assetStore.getAssetMap().getChildren(key);
        final HashSet<String> childrenIds = new HashSet<String>();
        if (children != null) {
            for (final Object child : children) {
                if (assetStore.getAssetMap().getPath(child) != null) {
                    childrenIds.add(child.toString());
                }
            }
        }
        this.getLogger().at(Level.INFO).log("Children ids for '%s': %s", key.toString(), childrenIds);
        editorClient.getPacketHandler().write(new AssetEditorRequestChildrenListReply(assetPath.toPacket(), childrenIds.toArray(String[]::new)));
    }
    
    public void handleDeleteAsset(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource == null) {
            editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (dataSource.isImmutable()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        final AssetTypeHandler assetTypeHandler = this.assetTypeRegistry.tryGetAssetTypeHandler(assetPath.path(), editorClient, requestToken);
        if (assetTypeHandler == null) {
            return;
        }
        final long stamp = this.globalEditLock.writeLock();
        try {
            if (!dataSource.doesAssetExist(assetPath.path())) {
                this.getLogger().at(Level.WARNING).log("%s does not exist", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.deleteAsset.alreadyDeleted"));
                return;
            }
            if (!dataSource.deleteAsset(assetPath.path(), editorClient)) {
                this.getLogger().at(Level.WARNING).log("Failed to delete %s from data source", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.failedToDeleteAsset"));
                return;
            }
            this.undoRedoManager.clearUndoRedoStack(assetPath);
            final AssetEditorFileEntry entry = dataSource.getAssetTree().removeAsset(assetPath.path());
            final AssetEditorAssetListUpdate packet = new AssetEditorAssetListUpdate(assetPath.packId(), null, new AssetEditorFileEntry[] { entry });
            editorClient.sendSuccessReply(requestToken);
            this.sendPacketToAllEditorUsersExcept(packet, editorClient);
            this.sendModifiedAssetsUpdateToConnectedUsers();
            assetTypeHandler.unloadAsset(assetPath);
        }
        finally {
            this.globalEditLock.unlockWrite(stamp);
        }
        this.getLogger().at(Level.INFO).log("Deleted asset %s", assetPath);
    }
    
    public void handleSubscribeToModifiedAssetsChanges(final EditorClient editorClient) {
        this.clientsSubscribedToModifiedAssetsChanges.add(editorClient);
    }
    
    public void handleUnsubscribeFromModifiedAssetsChanges(final EditorClient editorClient) {
        this.clientsSubscribedToModifiedAssetsChanges.remove(editorClient);
    }
    
    public void handleRenameAsset(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath oldAssetPath, @Nonnull final AssetPath newAssetPath, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(oldAssetPath);
        if (dataSource == null) {
            editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (dataSource.isImmutable()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        if (!this.isValidPath(dataSource, oldAssetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        if (!this.isValidPath(dataSource, newAssetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        final AssetTypeHandler assetTypeHandler = this.assetTypeRegistry.tryGetAssetTypeHandler(oldAssetPath.path(), editorClient, requestToken);
        if (assetTypeHandler == null) {
            return;
        }
        final String fileExtensionNew = PathUtil.getFileExtension(newAssetPath.path());
        if (!fileExtensionNew.equalsIgnoreCase(assetTypeHandler.getConfig().fileExtension)) {
            this.getLogger().at(Level.WARNING).log("File extension not matching. Expected %s, got %s", assetTypeHandler.getConfig().fileExtension, fileExtensionNew);
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.fileExtensionMismatch").param("fileExtension", assetTypeHandler.getConfig().fileExtension));
            return;
        }
        if (!newAssetPath.path().startsWith(assetTypeHandler.getRootPath())) {
            this.getLogger().at(Level.WARNING).log("%s is not within valid asset directory", newAssetPath);
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.directoryOutsideRoot"));
            return;
        }
        final long stamp = this.globalEditLock.writeLock();
        try {
            if (dataSource.doesAssetExist(newAssetPath.path())) {
                this.getLogger().at(Level.WARNING).log("%s already exists", newAssetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.renameAsset.alreadyExists"));
                return;
            }
            final byte[] oldAsset = dataSource.getAssetBytes(oldAssetPath.path());
            if (oldAsset == null) {
                this.getLogger().at(Level.WARNING).log("%s is not a regular file", oldAssetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.renameAsset.doesntExist"));
                return;
            }
            if (!dataSource.moveAsset(oldAssetPath.path(), newAssetPath.path(), editorClient)) {
                this.getLogger().at(Level.WARNING).log("Failed to move file %s to %s", oldAssetPath, newAssetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.renameAsset.failed"));
                return;
            }
            final AssetUndoRedoInfo undoRedo = this.undoRedoManager.clearUndoRedoStack(oldAssetPath);
            if (undoRedo != null) {
                this.undoRedoManager.putUndoRedoStack(newAssetPath, undoRedo);
            }
            this.getLogger().at(Level.WARNING).log("Moved %s to %s", oldAssetPath, newAssetPath);
            final AssetEditorFileEntry oldEntry = dataSource.getAssetTree().removeAsset(oldAssetPath.path());
            final AssetEditorFileEntry newEntry = dataSource.getAssetTree().ensureAsset(newAssetPath.path(), false);
            final AssetEditorAssetListUpdate packet = new AssetEditorAssetListUpdate(oldAssetPath.packId(), new AssetEditorFileEntry[] { newEntry }, new AssetEditorFileEntry[] { oldEntry });
            this.sendPacketToAllEditorUsersExcept(packet, editorClient);
            editorClient.sendSuccessReply(requestToken);
            assetTypeHandler.unloadAsset(oldAssetPath);
            assetTypeHandler.loadAsset(newAssetPath, dataSource.getFullPathToAssetData(newAssetPath.path()), oldAsset, editorClient);
        }
        finally {
            this.globalEditLock.unlockWrite(stamp);
        }
    }
    
    public void handleDeleteDirectory(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource.isImmutable()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.directoryOutsideRoot"));
            return;
        }
        if (!this.getAssetTypeRegistry().isPathInAssetTypeFolder(assetPath.path())) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        final long stamp = this.globalEditLock.writeLock();
        try {
            if (!dataSource.doesDirectoryExist(assetPath.path())) {
                this.getLogger().at(Level.WARNING).log("Directory doesn't exist %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.createDirectory.alreadyExists"));
                return;
            }
            if (!dataSource.getAssetTree().isDirectoryEmpty(assetPath.path())) {
                this.getLogger().at(Level.WARNING).log("%s must be empty", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.deleteDirectory.notEmpty"));
                return;
            }
            if (!dataSource.deleteDirectory(assetPath.path())) {
                this.getLogger().at(Level.WARNING).log("Directory %s could not be deleted!", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.deleteDirectory.failed"));
                return;
            }
            final AssetEditorFileEntry entry = dataSource.getAssetTree().removeAsset(assetPath.path());
            final AssetEditorAssetListUpdate packet = new AssetEditorAssetListUpdate(assetPath.packId(), null, new AssetEditorFileEntry[] { entry });
            this.sendPacketToAllEditorUsersExcept(packet, editorClient);
            editorClient.sendSuccessReply(requestToken);
            this.getLogger().at(Level.INFO).log("Deleted directory %s", assetPath);
        }
        finally {
            this.globalEditLock.unlockWrite(stamp);
        }
    }
    
    public void handleRenameDirectory(@Nonnull final EditorClient editorClient, final AssetPath path, final AssetPath newPath, final int requestToken) {
        editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.renameDirectory.unsupported"));
    }
    
    public void handleCreateDirectory(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource == null) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.createDirectory.noDataSource"));
            return;
        }
        if (dataSource.isImmutable()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.createDirectory.noPath"));
            return;
        }
        final long stamp = this.globalEditLock.writeLock();
        try {
            if (dataSource.doesDirectoryExist(assetPath.path())) {
                this.getLogger().at(Level.WARNING).log("Directory already exists at %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.createDirectory.alreadyExists"));
                return;
            }
            final Path parentDirectoryPath = assetPath.path().getParent();
            if (!dataSource.doesDirectoryExist(parentDirectoryPath)) {
                this.getLogger().at(Level.WARNING).log("Parent directory is missing for %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.parentDirectoryMissing"));
                return;
            }
            if (!dataSource.createDirectory(assetPath.path(), editorClient)) {
                this.getLogger().at(Level.WARNING).log("Failed to create directory %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.failedToCreateDirectory"));
                return;
            }
            final AssetEditorFileEntry entry = dataSource.getAssetTree().ensureAsset(assetPath.path(), true);
            if (entry != null) {
                final AssetEditorAssetListUpdate packet = new AssetEditorAssetListUpdate(assetPath.packId(), new AssetEditorFileEntry[] { entry }, null);
                this.sendPacketToAllEditorUsersExcept(packet, editorClient);
            }
            editorClient.sendSuccessReply(requestToken);
            this.getLogger().at(Level.WARNING).log("Created directory %s", assetPath);
        }
        finally {
            this.globalEditLock.unlockWrite(stamp);
        }
    }
    
    public void handleCreateAsset(@Nonnull final EditorClient editorClient, @Nonnull final AssetPath assetPath, @Nonnull final byte[] data, @Nonnull final AssetEditorRebuildCaches rebuildCaches, final String buttonId, final int requestToken) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        if (dataSource == null) {
            editorClient.sendFailureReply(requestToken, Messages.UNKNOWN_ASSETPACK_MESSAGE);
            return;
        }
        if (dataSource.isImmutable()) {
            editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.assetsReadOnly"));
            return;
        }
        if (!this.isValidPath(dataSource, assetPath)) {
            editorClient.sendFailureReply(requestToken, Messages.OUTSIDE_ASSET_ROOT_MESSAGE);
            return;
        }
        final AssetTypeHandler assetTypeHandler = this.assetTypeRegistry.tryGetAssetTypeHandler(assetPath.path(), editorClient, requestToken);
        if (assetTypeHandler == null) {
            return;
        }
        final long stamp = this.globalEditLock.writeLock();
        try {
            if (dataSource.doesAssetExist(assetPath.path())) {
                this.getLogger().at(Level.WARNING).log("%s already exists", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.createAsset.idAlreadyExists"));
                return;
            }
            if (!assetTypeHandler.isValidData(data)) {
                this.getLogger().at(Level.WARNING).log("Failed to validate data for %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.createAsset.failed"));
                return;
            }
            if (!dataSource.createAsset(assetPath.path(), data, editorClient)) {
                this.getLogger().at(Level.WARNING).log("Failed to create asset %s", assetPath);
                editorClient.sendFailureReply(requestToken, Message.translation("server.assetEditor.messages.createAsset.failed"));
                return;
            }
            this.getLogger().at(Level.INFO).log("Created asset %s", assetPath);
            final AssetEditorFileEntry entry = dataSource.getAssetTree().ensureAsset(assetPath.path(), false);
            if (entry != null) {
                final AssetEditorAssetListUpdate updatePacket = new AssetEditorAssetListUpdate(assetPath.packId(), new AssetEditorFileEntry[] { entry }, null);
                this.sendPacketToAllEditorUsersExcept(updatePacket, editorClient);
            }
            this.sendModifiedAssetsUpdateToConnectedUsers();
            final AssetUpdateQuery.RebuildCache rebuildCache = new AssetUpdateQuery.RebuildCache(rebuildCaches.blockTextures, rebuildCaches.models, rebuildCaches.modelTextures, rebuildCaches.mapGeometry, rebuildCaches.itemIcons, assetPath.path().startsWith(AssetPathUtil.PATH_DIR_COMMON));
            assetTypeHandler.loadAsset(assetPath, dataSource.getFullPathToAssetData(assetPath.path()), data, new AssetUpdateQuery(rebuildCache), editorClient);
            final IEventDispatcher<AssetEditorAssetCreatedEvent, AssetEditorAssetCreatedEvent> dispatch = HytaleServer.get().getEventBus().dispatchFor((Class<? super AssetEditorAssetCreatedEvent>)AssetEditorAssetCreatedEvent.class, assetTypeHandler.getConfig().id);
            if (dispatch.hasListener()) {
                dispatch.dispatch(new AssetEditorAssetCreatedEvent(editorClient, assetTypeHandler.getConfig().id, assetPath.path(), data, buttonId));
            }
            editorClient.sendSuccessReply(requestToken);
        }
        finally {
            this.globalEditLock.unlockWrite(stamp);
        }
    }
    
    private BsonDocument applyCommandsToAsset(@Nonnull final byte[] bytes, final AssetPath path, @Nonnull final JsonUpdateCommand[] commands, @Nonnull final AssetUpdateQuery.RebuildCacheBuilder rebuildCache) {
        BsonDocument asset = BsonDocument.parse(new String(bytes, StandardCharsets.UTF_8));
        this.getLogger().at(Level.INFO).log("Applying commands to %s with %s", path, asset);
        for (final JsonUpdateCommand command : commands) {
            switch (command.type) {
                case SetProperty: {
                    final BsonValue value = BsonDocument.parse(command.value).get("value");
                    this.getLogger().at(Level.INFO).log("Setting property %s to %s", String.join(".", (CharSequence[])command.path), value);
                    if (command.path.length > 0) {
                        BsonTransformationUtil.setProperty(asset, command.path, value);
                    }
                    else {
                        asset = (BsonDocument)value;
                    }
                    break;
                }
                case InsertProperty: {
                    final BsonValue value = BsonDocument.parse(command.value).get("value");
                    this.getLogger().at(Level.INFO).log("Inserting property %s with %s", String.join(".", (CharSequence[])command.path), value);
                    BsonTransformationUtil.insertProperty(asset, command.path, value);
                    break;
                }
                case RemoveProperty: {
                    this.getLogger().at(Level.INFO).log("Removing property %s", String.join(".", (CharSequence[])command.path));
                    BsonTransformationUtil.removeProperty(asset, command.path);
                    break;
                }
            }
        }
        this.getLogger().at(Level.INFO).log("Updated %s resulting: %s", path, asset);
        for (final JsonUpdateCommand command : commands) {
            if (command.rebuildCaches != null) {
                if (command.rebuildCaches.blockTextures) {
                    rebuildCache.setBlockTextures(true);
                }
                if (command.rebuildCaches.modelTextures) {
                    rebuildCache.setModelTextures(true);
                }
                if (command.rebuildCaches.models) {
                    rebuildCache.setModels(true);
                }
                if (command.rebuildCaches.mapGeometry) {
                    rebuildCache.setMapGeometry(true);
                }
                if (command.rebuildCaches.itemIcons) {
                    rebuildCache.setItemIcons(true);
                }
            }
        }
        return asset;
    }
    
    private void sendModifiedAssetsUpdateToConnectedUsers() {
        if (this.clientOpenAssetPathMapping.isEmpty()) {
            return;
        }
        if (!this.clientsSubscribedToModifiedAssetsChanges.isEmpty()) {
            final AssetEditorLastModifiedAssets lastModifiedAssetsPacket = this.buildAssetEditorLastModifiedAssetsPacket();
            for (final EditorClient p : this.clientsSubscribedToModifiedAssetsChanges) {
                p.getPacketHandler().write(lastModifiedAssetsPacket);
            }
        }
    }
    
    private void sendPacketToAllEditorUsers(@Nonnull final Packet packet) {
        for (final EditorClient editorClient : this.clientOpenAssetPathMapping.keySet()) {
            editorClient.getPacketHandler().write(packet);
        }
    }
    
    private void sendPacketToAllEditorUsersExcept(@Nonnull final Packet packet, final EditorClient ignoreEditorClient) {
        for (final EditorClient editorClient : this.clientOpenAssetPathMapping.keySet()) {
            if (editorClient.equals(ignoreEditorClient)) {
                continue;
            }
            editorClient.getPacketHandler().write(packet);
        }
    }
    
    private void updateAssetForConnectedClients(@Nonnull final AssetPath assetPath) {
        this.updateAssetForConnectedClients(assetPath, null);
    }
    
    private void updateAssetForConnectedClients(@Nonnull final AssetPath assetPath, final EditorClient ignoreEditorClient) {
        final DataSource dataSource = this.getDataSourceForPath(assetPath);
        final byte[] bytes = dataSource.getAssetBytes(assetPath.path());
        this.updateAssetForConnectedClients(assetPath, bytes, ignoreEditorClient);
    }
    
    private void updateAssetForConnectedClients(@Nonnull final AssetPath assetPath, final byte[] bytes, final EditorClient ignoreEditorClient) {
        final AssetEditorAssetUpdated updatePacket = new AssetEditorAssetUpdated(assetPath.toPacket(), bytes);
        for (final Map.Entry<EditorClient, AssetPath> entry : this.clientOpenAssetPathMapping.entrySet()) {
            if (entry.getKey().equals(ignoreEditorClient)) {
                continue;
            }
            if (!assetPath.equals(entry.getValue())) {
                continue;
            }
            entry.getKey().getPacketHandler().write(updatePacket);
        }
    }
    
    private void updateJsonAssetForConnectedClients(@Nonnull final AssetPath assetPath, final JsonUpdateCommand[] commands) {
        this.updateJsonAssetForConnectedClients(assetPath, commands, null);
    }
    
    private void updateJsonAssetForConnectedClients(@Nonnull final AssetPath assetPath, final JsonUpdateCommand[] commands, final EditorClient ignoreEditorClient) {
        final AssetEditorJsonAssetUpdated updatePacket = new AssetEditorJsonAssetUpdated(assetPath.toPacket(), commands);
        for (final Map.Entry<EditorClient, AssetPath> connectedPlayer : this.clientOpenAssetPathMapping.entrySet()) {
            if (connectedPlayer.getKey().equals(ignoreEditorClient)) {
                continue;
            }
            if (!assetPath.equals(connectedPlayer.getValue())) {
                continue;
            }
            connectedPlayer.getKey().getPacketHandler().write(updatePacket);
        }
    }
    
    @Nonnull
    private AssetEditorLastModifiedAssets buildAssetEditorLastModifiedAssetsPacket() {
        final ArrayList<AssetInfo> allAssets = new ArrayList<AssetInfo>();
        for (final Map.Entry<String, DataSource> dataSource : this.assetPackDataSources.entrySet()) {
            final StandardDataSource value = dataSource.getValue();
            if (value instanceof StandardDataSource) {
                final StandardDataSource standardDataSource = value;
                for (final ModifiedAsset assetInfo : standardDataSource.getRecentlyModifiedAssets().values()) {
                    allAssets.add(assetInfo.toAssetInfoPacket(dataSource.getKey()));
                }
            }
        }
        return new AssetEditorLastModifiedAssets(allAssets.toArray(new AssetInfo[0]));
    }
    
    boolean isValidPath(@Nonnull final DataSource dataSource, @Nonnull final AssetPath assetPath) {
        final String assetPathString = PathUtil.toUnixPathString(assetPath.path());
        final Path rootPath = dataSource.getRootPath();
        final Path absolutePath = rootPath.resolve(assetPathString).toAbsolutePath().normalize();
        if (!absolutePath.startsWith(rootPath)) {
            return false;
        }
        final String relativePath = PathUtil.toUnixPathString(rootPath.relativize(absolutePath));
        return relativePath.equals(assetPathString);
    }
    
    static {
        final SchemaContext schemaContext = new SchemaContext();
    }
    
    public static class AssetToDiscard
    {
        public final AssetPath path;
        @Nullable
        public final Instant lastModificationDate;
        
        public AssetToDiscard(final AssetPath path, @Nullable final String lastModificationDate) {
            this.path = path;
            if (lastModificationDate != null) {
                this.lastModificationDate = Instant.parse(lastModificationDate);
            }
            else {
                this.lastModificationDate = null;
            }
        }
    }
    
    enum DiscardResult
    {
        FAILED, 
        SUCCEEDED, 
        SUCCEEDED_COMMON_ASSETS_CHANGED;
    }
    
    enum InitState
    {
        NOT_INITIALIZED, 
        INITIALIZING, 
        INITIALIZED;
    }
}
