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

package com.hypixel.hytale.builtin.instances;

import com.hypixel.hytale.builtin.blockphysics.WorldValidationUtil;
import java.util.EnumSet;
import com.hypixel.hytale.server.core.universe.world.ValidationOption;
import com.hypixel.hytale.server.core.universe.world.storage.resources.IResourceStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.resources.EmptyResourceStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.provider.MigrationChunkStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.provider.EmptyChunkStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.provider.IChunkStorageProvider;
import joptsimple.OptionSpec;
import com.hypixel.hytale.server.core.Options;
import java.util.Map;
import com.hypixel.hytale.codec.schema.config.ObjectSchema;
import com.hypixel.hytale.codec.schema.config.StringSchema;
import com.hypixel.hytale.codec.schema.config.Schema;
import com.hypixel.hytale.server.core.universe.world.SoundUtil;
import com.hypixel.hytale.protocol.SoundCategory;
import com.hypixel.hytale.server.core.asset.type.soundevent.config.SoundEvent;
import com.hypixel.hytale.server.core.util.EventTitleUtil;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.builtin.instances.event.DiscoverInstanceEvent;
import java.util.Set;
import com.hypixel.hytale.builtin.instances.config.InstanceDiscoveryConfig;
import java.util.Collection;
import java.util.HashSet;
import com.hypixel.hytale.math.vector.Vector3f;
import com.hypixel.hytale.server.core.entity.entities.player.data.PlayerConfigData;
import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation;
import com.hypixel.hytale.server.core.entity.entities.Player;
import java.nio.file.FileVisitor;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.SimpleFileVisitor;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.List;
import com.hypixel.hytale.protocol.GameMode;
import java.util.Iterator;
import com.hypixel.hytale.assetstore.AssetPack;
import com.hypixel.hytale.server.core.asset.AssetModule;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport;
import com.hypixel.hytale.server.core.universe.world.spawn.ISpawnProvider;
import com.hypixel.hytale.logger.HytaleLogger;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Executor;
import java.util.Objects;
import com.hypixel.hytale.server.core.entity.UUIDComponent;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.component.Ref;
import java.util.stream.Stream;
import java.nio.file.Path;
import java.util.function.Function;
import com.hypixel.hytale.common.util.FormatUtil;
import com.hypixel.hytale.sneakythrow.SneakyThrow;
import java.nio.file.CopyOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.LinkOption;
import java.nio.file.Files;
import com.hypixel.hytale.server.core.util.io.FileUtil;
import java.util.logging.Level;
import com.hypixel.hytale.builtin.instances.config.WorldReturnPoint;
import java.util.UUID;
import com.hypixel.hytale.server.core.universe.Universe;
import javax.annotation.Nullable;
import java.util.concurrent.CompletableFuture;
import com.hypixel.hytale.math.vector.Transform;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.component.Holder;
import com.hypixel.hytale.component.ComponentRegistryProxy;
import com.hypixel.hytale.event.EventRegistry;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.builtin.instances.config.InstanceWorldConfig;
import com.hypixel.hytale.server.core.universe.world.WorldConfig;
import com.hypixel.hytale.server.core.plugin.PluginBase;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.server.OpenCustomUIInteraction;
import com.hypixel.hytale.builtin.instances.page.ConfigureInstanceBlockPage;
import com.hypixel.hytale.builtin.instances.config.ExitInstance;
import com.hypixel.hytale.server.core.asset.type.gameplay.respawn.RespawnController;
import com.hypixel.hytale.builtin.instances.interactions.ExitInstanceInteraction;
import com.hypixel.hytale.builtin.instances.interactions.TeleportConfigInstanceInteraction;
import com.hypixel.hytale.builtin.instances.interactions.TeleportInstanceInteraction;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction;
import com.hypixel.hytale.builtin.instances.removal.TimeoutCondition;
import com.hypixel.hytale.builtin.instances.removal.IdleTimeoutCondition;
import com.hypixel.hytale.builtin.instances.removal.WorldEmptyCondition;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.codec.lookup.StringCodecMapCodec;
import com.hypixel.hytale.builtin.instances.removal.RemovalCondition;
import com.hypixel.hytale.builtin.instances.removal.RemovalSystem;
import com.hypixel.hytale.component.system.ISystem;
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
import com.hypixel.hytale.server.core.event.events.player.DrainPlayerFromWorldEvent;
import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent;
import com.hypixel.hytale.server.core.asset.GenerateSchemaEvent;
import com.hypixel.hytale.server.core.asset.LoadAssetEvent;
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.builtin.instances.command.InstancesCommand;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.builtin.instances.blocks.ConfigurableInstanceBlock;
import com.hypixel.hytale.builtin.instances.blocks.InstanceBlock;
import com.hypixel.hytale.builtin.instances.config.InstanceEntityConfig;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.builtin.instances.removal.InstanceDataResource;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.component.ResourceType;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;

public class InstancesPlugin extends JavaPlugin
{
    private static InstancesPlugin instance;
    @Nonnull
    public static final String INSTANCE_PREFIX = "instance-";
    @Nonnull
    public static final String CONFIG_FILENAME = "instance.bson";
    private ResourceType<ChunkStore, InstanceDataResource> instanceDataResourceType;
    private ComponentType<EntityStore, InstanceEntityConfig> instanceEntityConfigComponentType;
    private ComponentType<ChunkStore, InstanceBlock> instanceBlockComponentType;
    private ComponentType<ChunkStore, ConfigurableInstanceBlock> configurableInstanceBlockComponentType;
    
    public static InstancesPlugin get() {
        return InstancesPlugin.instance;
    }
    
    public InstancesPlugin(@Nonnull final JavaPluginInit init) {
        super(init);
        InstancesPlugin.instance = this;
    }
    
    @Override
    protected void setup() {
        final EventRegistry eventRegistry = this.getEventRegistry();
        final ComponentRegistryProxy<ChunkStore> chunkStoreRegistry = this.getChunkStoreRegistry();
        this.getCommandRegistry().registerCommand(new InstancesCommand());
        eventRegistry.register((short)64, LoadAssetEvent.class, this::validateInstanceAssets);
        eventRegistry.register(GenerateSchemaEvent.class, InstancesPlugin::generateSchema);
        eventRegistry.registerGlobal(AddPlayerToWorldEvent.class, InstancesPlugin::onPlayerAddToWorld);
        eventRegistry.registerGlobal(DrainPlayerFromWorldEvent.class, InstancesPlugin::onPlayerDrainFromWorld);
        eventRegistry.register(PlayerConnectEvent.class, InstancesPlugin::onPlayerConnect);
        eventRegistry.registerGlobal(PlayerReadyEvent.class, InstancesPlugin::onPlayerReady);
        this.instanceBlockComponentType = chunkStoreRegistry.registerComponent(InstanceBlock.class, "Instance", InstanceBlock.CODEC);
        chunkStoreRegistry.registerSystem(new InstanceBlock.OnRemove());
        this.configurableInstanceBlockComponentType = chunkStoreRegistry.registerComponent(ConfigurableInstanceBlock.class, "InstanceConfig", ConfigurableInstanceBlock.CODEC);
        chunkStoreRegistry.registerSystem(new ConfigurableInstanceBlock.OnRemove());
        this.instanceDataResourceType = chunkStoreRegistry.registerResource(InstanceDataResource.class, "InstanceData", InstanceDataResource.CODEC);
        chunkStoreRegistry.registerSystem(new RemovalSystem());
        this.instanceEntityConfigComponentType = this.getEntityStoreRegistry().registerComponent(InstanceEntityConfig.class, "Instance", InstanceEntityConfig.CODEC);
        this.getCodecRegistry((StringCodecMapCodec<Object, BuilderCodec<WorldEmptyCondition>>)RemovalCondition.CODEC).register("WorldEmpty", WorldEmptyCondition.class, WorldEmptyCondition.CODEC).register("IdleTimeout", IdleTimeoutCondition.class, (BuilderCodec<WorldEmptyCondition>)IdleTimeoutCondition.CODEC).register("Timeout", TimeoutCondition.class, (BuilderCodec<WorldEmptyCondition>)TimeoutCondition.CODEC);
        this.getCodecRegistry(Interaction.CODEC).register("TeleportInstance", TeleportInstanceInteraction.class, TeleportInstanceInteraction.CODEC).register("TeleportConfigInstance", TeleportConfigInstanceInteraction.class, TeleportConfigInstanceInteraction.CODEC).register("ExitInstance", ExitInstanceInteraction.class, ExitInstanceInteraction.CODEC);
        this.getCodecRegistry((StringCodecMapCodec<Object, BuilderCodec<ExitInstance>>)RespawnController.CODEC).register("ExitInstance", ExitInstance.class, ExitInstance.CODEC);
        OpenCustomUIInteraction.registerBlockEntityCustomPage(this, ConfigureInstanceBlockPage.class, "ConfigInstanceBlock", ConfigureInstanceBlockPage::new, () -> {
            final Holder<ChunkStore> holder = ChunkStore.REGISTRY.newHolder();
            holder.ensureComponent(ConfigurableInstanceBlock.getComponentType());
            return holder;
        });
        this.getCodecRegistry(WorldConfig.PLUGIN_CODEC).register(InstanceWorldConfig.class, "Instance", InstanceWorldConfig.CODEC);
    }
    
    @Nonnull
    public CompletableFuture<World> spawnInstance(@Nonnull final String name, @Nonnull final World forWorld, @Nonnull final Transform returnPoint) {
        return this.spawnInstance(name, null, forWorld, returnPoint);
    }
    
    @Nonnull
    public CompletableFuture<World> spawnInstance(@Nonnull final String name, @Nullable final String worldName, @Nonnull final World forWorld, @Nonnull final Transform returnPoint) {
        final Universe universe = Universe.get();
        final Path path = universe.getPath();
        final Path assetPath = getInstanceAssetPath(name);
        final UUID uuid = UUID.randomUUID();
        String worldKey = worldName;
        if (worldKey == null) {
            worldKey = "instance-" + safeName(name) + "-" + String.valueOf(uuid);
        }
        final Path worldPath = path.resolve("worlds").resolve(worldKey);
        final String finalWorldKey = worldKey;
        return WorldConfig.load(assetPath.resolve("instance.bson")).thenApplyAsync((Function<? super WorldConfig, ?>)SneakyThrow.sneakyFunction(config -> {
            config.setUuid(uuid);
            config.setDisplayName(WorldConfig.formatDisplayName(name));
            final InstanceWorldConfig instanceConfig = InstanceWorldConfig.ensureAndGet(config);
            instanceConfig.setReturnPoint(new WorldReturnPoint(forWorld.getWorldConfig().getUuid(), returnPoint, instanceConfig.shouldPreventReconnection()));
            config.markChanged();
            final long start = System.nanoTime();
            this.getLogger().at(Level.INFO).log("Copying instance files for %s to world %s", name, finalWorldKey);
            try (final Stream<Path> files = Files.walk(assetPath, FileUtil.DEFAULT_WALK_TREE_OPTIONS_ARRAY)) {
                files.forEach(SneakyThrow.sneakyConsumer(filePath -> {
                    final Path rel = assetPath.relativize(filePath);
                    final Path toPath = worldPath.resolve(rel.toString());
                    if (Files.isDirectory(filePath, new LinkOption[0])) {
                        Files.createDirectories(toPath, (FileAttribute<?>[])new FileAttribute[0]);
                        return;
                    }
                    else {
                        if (Files.isRegularFile(filePath, new LinkOption[0])) {
                            Files.copy(filePath, toPath, new CopyOption[0]);
                        }
                        return;
                    }
                }));
            }
            this.getLogger().at(Level.INFO).log("Completed instance files for %s to world %s in %s", name, finalWorldKey, FormatUtil.nanosToString(System.nanoTime() - start));
            return config;
        })).thenCompose(config -> universe.makeWorld(finalWorldKey, worldPath, config));
    }
    
    public static void teleportPlayerToLoadingInstance(@Nonnull final Ref<EntityStore> entityRef, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nonnull final CompletableFuture<World> worldFuture, @Nullable final Transform overrideReturn) {
        final World originalWorld = componentAccessor.getExternalData().getWorld();
        final TransformComponent transformComponent = componentAccessor.getComponent(entityRef, TransformComponent.getComponentType());
        assert transformComponent != null;
        final Transform originalPosition = transformComponent.getTransform().clone();
        InstanceEntityConfig instanceEntityConfigComponent = componentAccessor.getComponent(entityRef, InstanceEntityConfig.getComponentType());
        if (instanceEntityConfigComponent == null) {
            instanceEntityConfigComponent = componentAccessor.addComponent(entityRef, InstanceEntityConfig.getComponentType());
        }
        if (overrideReturn != null) {
            instanceEntityConfigComponent.setReturnPointOverride(new WorldReturnPoint(originalWorld.getWorldConfig().getUuid(), overrideReturn, false));
        }
        else {
            instanceEntityConfigComponent.setReturnPointOverride(null);
        }
        final PlayerRef playerRefComponent = componentAccessor.getComponent(entityRef, PlayerRef.getComponentType());
        assert playerRefComponent != null;
        final UUIDComponent uuidComponent = componentAccessor.getComponent(entityRef, UUIDComponent.getComponentType());
        assert uuidComponent != null;
        final UUID playerUUID = uuidComponent.getUuid();
        final InstanceEntityConfig finalPlayerConfig = instanceEntityConfigComponent;
        final PlayerRef obj = playerRefComponent;
        Objects.requireNonNull(obj);
        CompletableFuture.runAsync(obj::removeFromStore, originalWorld).thenCombine((CompletionStage<?>)worldFuture.orTimeout(1L, TimeUnit.MINUTES), (ignored, world) -> world).thenCompose(world -> {
            final ISpawnProvider spawnProvider = world.getWorldConfig().getSpawnProvider();
            final Transform spawnPoint = (spawnProvider != null) ? spawnProvider.getSpawnPoint(world, playerUUID) : null;
            return world.addPlayer(playerRefComponent, spawnPoint, Boolean.TRUE, Boolean.FALSE);
        }).whenComplete((ret, ex) -> {
            if (ex != null) {
                get().getLogger().at(Level.SEVERE).withCause(ex).log("Failed to send %s to instance world", playerRefComponent.getUsername());
                finalPlayerConfig.setReturnPointOverride(null);
            }
            if (ret == null) {
                if (originalWorld.isAlive()) {
                    originalWorld.addPlayer(playerRefComponent, originalPosition, Boolean.TRUE, Boolean.FALSE);
                }
                else {
                    final World defaultWorld = Universe.get().getDefaultWorld();
                    if (defaultWorld != null) {
                        defaultWorld.addPlayer(playerRefComponent, null, Boolean.TRUE, Boolean.FALSE);
                    }
                    else {
                        get().getLogger().at(Level.SEVERE).log("No fallback world for %s, disconnecting", playerRefComponent.getUsername());
                        playerRefComponent.getPacketHandler().disconnect("Failed to teleport - no world available");
                    }
                }
            }
        });
    }
    
    public static void teleportPlayerToInstance(@Nonnull final Ref<EntityStore> playerRef, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nonnull final World targetWorld, @Nullable final Transform overrideReturn) {
        final World originalWorld = componentAccessor.getExternalData().getWorld();
        final WorldConfig originalWorldConfig = originalWorld.getWorldConfig();
        if (overrideReturn != null) {
            final InstanceEntityConfig instanceConfig = componentAccessor.ensureAndGetComponent(playerRef, InstanceEntityConfig.getComponentType());
            instanceConfig.setReturnPointOverride(new WorldReturnPoint(originalWorldConfig.getUuid(), overrideReturn, false));
        }
        final UUIDComponent uuidComponent = componentAccessor.getComponent(playerRef, UUIDComponent.getComponentType());
        assert uuidComponent != null;
        final UUID playerUUID = uuidComponent.getUuid();
        final WorldConfig targetWorldConfig = targetWorld.getWorldConfig();
        final ISpawnProvider spawnProvider = targetWorldConfig.getSpawnProvider();
        if (spawnProvider == null) {
            throw new IllegalStateException("Spawn provider cannot be null when teleporting player to instance!");
        }
        final Transform spawnTransform = spawnProvider.getSpawnPoint(targetWorld, playerUUID);
        final Teleport teleportComponent = Teleport.createForPlayer(targetWorld, spawnTransform);
        componentAccessor.addComponent(playerRef, Teleport.getComponentType(), teleportComponent);
    }
    
    public static void exitInstance(@Nonnull final Ref<EntityStore> targetRef, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final World world = componentAccessor.getExternalData().getWorld();
        final InstanceEntityConfig entityConfig = componentAccessor.getComponent(targetRef, InstanceEntityConfig.getComponentType());
        WorldReturnPoint returnPoint = (entityConfig != null) ? entityConfig.getReturnPoint() : null;
        if (returnPoint == null) {
            final WorldConfig config = world.getWorldConfig();
            final InstanceWorldConfig instanceConfig = InstanceWorldConfig.get(config);
            returnPoint = ((instanceConfig != null) ? instanceConfig.getReturnPoint() : null);
            if (returnPoint == null) {
                throw new IllegalArgumentException("Player is not in an instance");
            }
        }
        final Universe universe = Universe.get();
        final World targetWorld = universe.getWorld(returnPoint.getWorld());
        if (targetWorld == null) {
            throw new IllegalArgumentException("Missing return world");
        }
        final Teleport teleportComponent = Teleport.createForPlayer(targetWorld, returnPoint.getReturnPoint());
        componentAccessor.addComponent(targetRef, Teleport.getComponentType(), teleportComponent);
    }
    
    public static void safeRemoveInstance(@Nonnull final String worldName) {
        safeRemoveInstance(Universe.get().getWorld(worldName));
    }
    
    public static void safeRemoveInstance(@Nonnull final UUID worldUUID) {
        safeRemoveInstance(Universe.get().getWorld(worldUUID));
    }
    
    public static void safeRemoveInstance(@Nullable final World instanceWorld) {
        if (instanceWorld == null) {
            return;
        }
        final Store<ChunkStore> chunkStore = instanceWorld.getChunkStore().getStore();
        chunkStore.getResource(InstanceDataResource.getResourceType()).setHadPlayer(true);
        final WorldConfig config = instanceWorld.getWorldConfig();
        final InstanceWorldConfig instanceConfig = InstanceWorldConfig.get(config);
        if (instanceConfig != null) {
            instanceConfig.setRemovalConditions(WorldEmptyCondition.REMOVE_WHEN_EMPTY);
        }
        config.markChanged();
    }
    
    @Nonnull
    public static Path getInstanceAssetPath(@Nonnull final String name) {
        for (final AssetPack pack : AssetModule.get().getAssetPacks()) {
            final Path path = pack.getRoot().resolve("Server").resolve("Instances").resolve(name);
            if (Files.exists(path, new LinkOption[0])) {
                return path;
            }
        }
        return AssetModule.get().getBaseAssetPack().getRoot().resolve("Server").resolve("Instances").resolve(name);
    }
    
    public static boolean doesInstanceAssetExist(@Nonnull final String name) {
        return Files.exists(getInstanceAssetPath(name).resolve("instance.bson"), new LinkOption[0]);
    }
    
    @Nonnull
    public static CompletableFuture<World> loadInstanceAssetForEdit(@Nonnull final String name) {
        final Path path = getInstanceAssetPath(name);
        final Universe universe = Universe.get();
        return WorldConfig.load(path.resolve("instance.bson")).thenCompose(config -> {
            config.setUuid(UUID.randomUUID());
            config.setSavingPlayers(false);
            config.setIsAllNPCFrozen(true);
            config.setTicking(false);
            config.setGameMode(GameMode.Creative);
            config.setDeleteOnRemove(false);
            InstanceWorldConfig.ensureAndGet(config).setRemovalConditions(RemovalCondition.EMPTY);
            config.markChanged();
            final String worldName = "instance-edit-" + safeName(name);
            return universe.makeWorld(worldName, path, config);
        });
    }
    
    @Nonnull
    public List<String> getInstanceAssets() {
        final List<String> instances = new ObjectArrayList<String>();
        for (final AssetPack pack : AssetModule.get().getAssetPacks()) {
            final Path path = pack.getRoot().resolve("Server").resolve("Instances");
            if (!Files.isDirectory(path, new LinkOption[0])) {
                continue;
            }
            try {
                Files.walkFileTree(path, FileUtil.DEFAULT_WALK_TREE_OPTIONS_SET, Integer.MAX_VALUE, new SimpleFileVisitor<Path>(this) {
                    @Nonnull
                    @Override
                    public FileVisitResult preVisitDirectory(@Nonnull final Path dir, @Nonnull final BasicFileAttributes attrs) {
                        if (Files.exists(dir.resolve("instance.bson"), new LinkOption[0])) {
                            final Path relative = path.relativize(dir);
                            final String name = relative.toString();
                            instances.add(name);
                            return FileVisitResult.SKIP_SUBTREE;
                        }
                        return FileVisitResult.CONTINUE;
                    }
                });
            }
            catch (final IOException e) {
                throw SneakyThrow.sneakyThrow(e);
            }
        }
        return instances;
    }
    
    private static void onPlayerConnect(@Nonnull final PlayerConnectEvent event) {
        final Holder<EntityStore> holder = event.getHolder();
        final Player playerComponent = holder.getComponent(Player.getComponentType());
        assert playerComponent != null;
        final PlayerConfigData playerConfig = playerComponent.getPlayerConfigData();
        final InstanceEntityConfig config = InstanceEntityConfig.ensureAndGet(holder);
        final String lastWorldName = playerConfig.getWorld();
        World lastWorld = Universe.get().getWorld(lastWorldName);
        final WorldReturnPoint fallbackWorld = config.getReturnPoint();
        if (fallbackWorld != null && (lastWorld == null || fallbackWorld.isReturnOnReconnect())) {
            lastWorld = Universe.get().getWorld(fallbackWorld.getWorld());
            if (lastWorld != null) {
                final Transform transform = fallbackWorld.getReturnPoint();
                final TransformComponent transformComponent = holder.ensureAndGetComponent(TransformComponent.getComponentType());
                transformComponent.setPosition(transform.getPosition());
                final Vector3f rotationClone = transformComponent.getRotation().clone();
                rotationClone.setYaw(transform.getRotation().getYaw());
                transformComponent.setRotation(rotationClone);
                final HeadRotation headRotationComponent = holder.ensureAndGetComponent(HeadRotation.getComponentType());
                headRotationComponent.teleportRotation(transform.getRotation());
            }
        }
        else if (lastWorld != null) {
            config.setReturnPointOverride(config.getReturnPoint());
        }
    }
    
    private static void onPlayerAddToWorld(@Nonnull final AddPlayerToWorldEvent event) {
        final Holder<EntityStore> holder = event.getHolder();
        final InstanceWorldConfig worldConfig = InstanceWorldConfig.get(event.getWorld().getWorldConfig());
        if (worldConfig == null) {
            final InstanceEntityConfig entityConfig = holder.getComponent(InstanceEntityConfig.getComponentType());
            if (entityConfig != null && entityConfig.getReturnPoint() != null) {
                entityConfig.setReturnPoint(null);
            }
            return;
        }
        final InstanceEntityConfig entityConfig = InstanceEntityConfig.ensureAndGet(holder);
        if (entityConfig.getReturnPointOverride() == null) {
            entityConfig.setReturnPoint(worldConfig.getReturnPoint());
        }
        else {
            final WorldReturnPoint override = entityConfig.getReturnPointOverride();
            override.setReturnOnReconnect(worldConfig.shouldPreventReconnection());
            entityConfig.setReturnPoint(override);
            entityConfig.setReturnPointOverride(null);
        }
    }
    
    private static void onPlayerReady(@Nonnull final PlayerReadyEvent event) {
        final Player player = event.getPlayer();
        final World world = player.getWorld();
        if (world == null) {
            return;
        }
        final WorldConfig worldConfig = world.getWorldConfig();
        final InstanceWorldConfig instanceWorldConfig = InstanceWorldConfig.get(worldConfig);
        if (instanceWorldConfig == null) {
            return;
        }
        final InstanceDiscoveryConfig discoveryConfig = instanceWorldConfig.getDiscovery();
        if (discoveryConfig == null) {
            return;
        }
        final PlayerConfigData playerConfigData = player.getPlayerConfigData();
        final UUID instanceUuid = worldConfig.getUuid();
        if (!discoveryConfig.alwaysDisplay() && playerConfigData.getDiscoveredInstances().contains(instanceUuid)) {
            return;
        }
        final Set<UUID> discoveredInstances = new HashSet<UUID>(playerConfigData.getDiscoveredInstances());
        discoveredInstances.add(instanceUuid);
        playerConfigData.setDiscoveredInstances(discoveredInstances);
        final Ref<EntityStore> playerRef = event.getPlayerRef();
        if (!playerRef.isValid()) {
            return;
        }
        world.execute(() -> {
            final Store<EntityStore> store = world.getEntityStore().getStore();
            showInstanceDiscovery(playerRef, store, instanceUuid, discoveryConfig);
        });
    }
    
    private static void showInstanceDiscovery(@Nonnull final Ref<EntityStore> ref, @Nonnull final Store<EntityStore> store, @Nonnull final UUID instanceUuid, @Nonnull InstanceDiscoveryConfig discoveryConfig) {
        final DiscoverInstanceEvent.Display discoverInstanceEvent = new DiscoverInstanceEvent.Display(instanceUuid, discoveryConfig.clone());
        store.invoke(ref, discoverInstanceEvent);
        discoveryConfig = discoverInstanceEvent.getDiscoveryConfig();
        if (discoverInstanceEvent.isCancelled() || !discoverInstanceEvent.shouldDisplay()) {
            return;
        }
        final PlayerRef playerRefComponent = store.getComponent(ref, PlayerRef.getComponentType());
        if (playerRefComponent == null) {
            return;
        }
        final String subtitleKey = discoveryConfig.getSubtitleKey();
        final Message subtitle = (subtitleKey != null) ? Message.translation(subtitleKey) : Message.empty();
        EventTitleUtil.showEventTitleToPlayer(playerRefComponent, Message.translation(discoveryConfig.getTitleKey()), subtitle, discoveryConfig.isMajor(), discoveryConfig.getIcon(), discoveryConfig.getDuration(), discoveryConfig.getFadeInDuration(), discoveryConfig.getFadeOutDuration());
        final String discoverySoundEventId = discoveryConfig.getDiscoverySoundEventId();
        if (discoverySoundEventId != null) {
            final int assetIndex = SoundEvent.getAssetMap().getIndex(discoverySoundEventId);
            if (assetIndex != Integer.MIN_VALUE) {
                SoundUtil.playSoundEvent2d(ref, assetIndex, SoundCategory.UI, store);
            }
        }
    }
    
    private static void onPlayerDrainFromWorld(@Nonnull final DrainPlayerFromWorldEvent event) {
        final InstanceEntityConfig config = InstanceEntityConfig.removeAndGet(event.getHolder());
        if (config == null) {
            return;
        }
        final WorldReturnPoint returnPoint = config.getReturnPoint();
        if (returnPoint == null) {
            return;
        }
        final World returnWorld = Universe.get().getWorld(returnPoint.getWorld());
        if (returnWorld == null) {
            return;
        }
        event.setWorld(returnWorld);
        event.setTransform(returnPoint.getReturnPoint());
    }
    
    private static void generateSchema(@Nonnull final GenerateSchemaEvent event) {
        final ObjectSchema worldConfig = WorldConfig.CODEC.toSchema(event.getContext());
        final Map<String, Schema> props = worldConfig.getProperties();
        props.put("UUID", Schema.anyOf(new StringSchema(), new ObjectSchema()));
        worldConfig.setTitle("Instance Configuration");
        worldConfig.setId("InstanceConfig.json");
        final Schema.HytaleMetadata hytaleMetadata = worldConfig.getHytale();
        if (hytaleMetadata != null) {
            hytaleMetadata.setPath("Instances");
            hytaleMetadata.setExtension("instance.bson");
            hytaleMetadata.setUiEditorIgnore(Boolean.TRUE);
        }
        event.addSchema("InstanceConfig.json", worldConfig);
        event.addSchemaLink("InstanceConfig", List.of("Instances/**/instance.bson"), ".bson");
    }
    
    private void validateInstanceAssets(@Nonnull final LoadAssetEvent event) {
        final Path path = AssetModule.get().getBaseAssetPack().getRoot().resolve("Server").resolve("Instances");
        if (!Options.getOptionSet().has(Options.VALIDATE_ASSETS) || !Files.isDirectory(path, new LinkOption[0]) || event.isShouldShutdown()) {
            return;
        }
        final StringBuilder errors = new StringBuilder();
        final List<String> instanceAssets = this.getInstanceAssets();
        for (String name : instanceAssets) {
            final StringBuilder sb = new StringBuilder();
            final Path instancePath = getInstanceAssetPath(name);
            final Universe universe = Universe.get();
            final WorldConfig config = WorldConfig.load(instancePath.resolve("instance.bson")).join();
            final IChunkStorageProvider storage = config.getChunkStorageProvider();
            config.setChunkStorageProvider(new MigrationChunkStorageProvider(new IChunkStorageProvider[] { storage }, EmptyChunkStorageProvider.INSTANCE));
            config.setResourceStorageProvider(EmptyResourceStorageProvider.INSTANCE);
            config.setUuid(UUID.randomUUID());
            config.setSavingPlayers(false);
            config.setIsAllNPCFrozen(true);
            config.setSavingConfig(false);
            config.setTicking(false);
            config.setGameMode(GameMode.Creative);
            config.setDeleteOnRemove(false);
            config.setCompassUpdating(false);
            InstanceWorldConfig.ensureAndGet(config).setRemovalConditions(RemovalCondition.EMPTY);
            config.markChanged();
            final String worldName = "instance-validate-" + safeName(name);
            try {
                final World world = universe.makeWorld(worldName, instancePath, config, false).join();
                final EnumSet<ValidationOption> options = EnumSet.of(ValidationOption.BLOCK_STATES, ValidationOption.BLOCKS);
                world.validate(sb, WorldValidationUtil.blockValidator(errors, options), options);
            }
            catch (final Exception e) {
                sb.append("\t").append(e.getMessage());
                this.getLogger().at(Level.SEVERE).withCause(e).log("Failed to validate: " + name);
            }
            finally {
                if (!sb.isEmpty()) {
                    errors.append("Instance: ").append(name).append('\n').append((CharSequence)sb).append('\n');
                }
            }
            if (universe.getWorld(worldName) != null) {
                universe.removeWorld(worldName);
            }
        }
        if (!errors.isEmpty()) {
            this.getLogger().at(Level.SEVERE).log("Failed to validate instances:\n" + String.valueOf(errors));
            event.failed(true, "failed to validate instances");
        }
        HytaleLogger.getLogger().at(Level.INFO).log("Loading Instance assets phase completed! Boot time %s", FormatUtil.nanosToString(System.nanoTime() - event.getBootStart()));
    }
    
    @Nonnull
    public static String safeName(@Nonnull final String name) {
        return name.replace('/', '-');
    }
    
    @Nonnull
    public ResourceType<ChunkStore, InstanceDataResource> getInstanceDataResourceType() {
        return this.instanceDataResourceType;
    }
    
    @Nonnull
    public ComponentType<EntityStore, InstanceEntityConfig> getInstanceEntityConfigComponentType() {
        return this.instanceEntityConfigComponentType;
    }
    
    @Nonnull
    public ComponentType<ChunkStore, InstanceBlock> getInstanceBlockComponentType() {
        return this.instanceBlockComponentType;
    }
    
    @Nonnull
    public ComponentType<ChunkStore, ConfigurableInstanceBlock> getConfigurableInstanceBlockComponentType() {
        return this.configurableInstanceBlockComponentType;
    }
}
