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

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

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
import joptsimple.OptionSet;
import com.hypixel.hytale.server.core.util.AssetUtil;
import com.hypixel.hytale.server.core.modules.singleplayer.SingleplayerModule;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
import com.hypixel.hytale.event.IEventDispatcher;
import com.hypixel.hytale.server.core.entity.entities.player.data.PlayerConfigData;
import com.hypixel.hytale.math.vector.Transform;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.protocol.packets.setup.ServerTags;
import com.hypixel.hytale.assetstore.AssetRegistry;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
import com.hypixel.hytale.server.core.receiver.IPacketReceiver;
import com.hypixel.hytale.server.core.modules.entity.tracker.EntityTrackerSystems;
import com.hypixel.hytale.server.core.io.netty.NettyUtil;
import com.hypixel.hytale.server.core.cosmetics.CosmeticsModule;
import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent;
import com.hypixel.hytale.server.core.modules.entity.player.PlayerSkinComponent;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.modules.entity.component.MovementAudioComponent;
import com.hypixel.hytale.server.core.modules.entity.component.PositionDataComponent;
import com.hypixel.hytale.server.core.entity.UUIDComponent;
import com.hypixel.hytale.server.core.io.PacketHandler;
import com.hypixel.hytale.component.Holder;
import com.hypixel.hytale.server.core.modules.entity.player.ChunkTracker;
import com.hypixel.hytale.server.core.io.handlers.game.GamePacketHandler;
import com.hypixel.hytale.protocol.PlayerSkin;
import com.hypixel.hytale.server.core.auth.PlayerAuthentication;
import com.hypixel.hytale.server.core.io.ProtocolVersion;
import io.netty.channel.Channel;
import java.util.function.BiPredicate;
import java.util.Comparator;
import com.hypixel.hytale.server.core.NameMatching;
import java.util.Collection;
import java.util.List;
import com.hypixel.hytale.event.IEvent;
import com.hypixel.hytale.server.core.universe.world.events.RemoveWorldEvent;
import java.util.Objects;
import com.hypixel.hytale.server.core.util.io.FileUtil;
import java.util.function.Supplier;
import com.hypixel.hytale.sneakythrow.SneakyThrow;
import java.util.ConcurrentModificationException;
import com.hypixel.hytale.server.core.universe.world.events.AddWorldEvent;
import com.hypixel.hytale.common.semver.SemverRange;
import com.hypixel.hytale.common.plugin.PluginIdentifier;
import com.hypixel.hytale.server.core.plugin.PluginManager;
import com.hypixel.hytale.server.core.universe.world.WorldConfig;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import javax.annotation.Nullable;
import javax.annotation.CheckReturnValue;
import com.hypixel.hytale.metrics.MetricResults;
import java.nio.file.DirectoryStream;
import org.bson.BsonDocument;
import java.util.Iterator;
import com.hypixel.hytale.server.core.HytaleServerConfig;
import com.hypixel.hytale.common.util.CompletableFutureUtil;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.server.core.universe.world.events.AllWorldsLoadedEvent;
import org.bson.BsonValue;
import com.hypixel.hytale.server.core.util.BsonUtil;
import org.bson.BsonArray;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.nio.file.StandardCopyOption;
import java.nio.file.CopyOption;
import com.hypixel.hytale.server.core.event.events.PrepareUniverseEvent;
import com.hypixel.hytale.server.core.command.system.CommandRegistry;
import com.hypixel.hytale.component.ComponentRegistryProxy;
import com.hypixel.hytale.event.EventRegistry;
import com.hypixel.hytale.server.core.universe.world.commands.world.WorldCommand;
import com.hypixel.hytale.server.core.universe.world.commands.block.BlockSelectCommand;
import com.hypixel.hytale.server.core.universe.world.commands.block.BlockCommand;
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.server.core.universe.world.commands.SetTickingCommand;
import com.hypixel.hytale.server.core.universe.system.PlayerRefAddedSystem;
import com.hypixel.hytale.server.core.modules.entity.player.PlayerConnectionFlushSystem;
import com.hypixel.hytale.server.core.modules.entity.player.PlayerPingSystem;
import com.hypixel.hytale.server.core.universe.system.WorldConfigSaveSystem;
import com.hypixel.hytale.server.core.universe.world.system.WorldPregenerateSystem;
import com.hypixel.hytale.component.system.ISystem;
import com.hypixel.hytale.server.core.universe.world.storage.resources.EmptyResourceStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.resources.DiskResourceStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.resources.DefaultResourceStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.resources.IResourceStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.provider.EmptyChunkStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.provider.MigrationChunkStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.provider.DefaultChunkStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.provider.IChunkStorageProvider;
import com.hypixel.hytale.server.core.universe.world.worldmap.provider.chunk.WorldGenWorldMapProvider;
import com.hypixel.hytale.server.core.universe.world.worldmap.provider.DisabledWorldMapProvider;
import com.hypixel.hytale.server.core.universe.world.worldmap.provider.IWorldMapProvider;
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.VoidWorldGenProvider;
import com.hypixel.hytale.codec.lookup.Priority;
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.DummyWorldGenProvider;
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.FlatWorldGenProvider;
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.IWorldGenProvider;
import com.hypixel.hytale.server.core.universe.world.spawn.FitToHeightMapSpawnProvider;
import com.hypixel.hytale.server.core.universe.world.spawn.IndividualSpawnProvider;
import com.hypixel.hytale.server.core.universe.world.spawn.GlobalSpawnProvider;
import com.hypixel.hytale.server.core.universe.world.spawn.ISpawnProvider;
import com.hypixel.hytale.server.core.event.events.ShutdownEvent;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.util.backup.BackupTask;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
import java.util.concurrent.Executor;
import com.hypixel.hytale.server.core.universe.world.storage.component.ChunkSavingSystems;
import java.util.concurrent.TimeUnit;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.HytaleServer;
import java.util.logging.Level;
import java.io.IOException;
import java.nio.file.attribute.FileAttribute;
import joptsimple.OptionSpec;
import com.hypixel.hytale.server.core.Options;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import com.hypixel.hytale.server.core.Constants;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import java.util.concurrent.CompletableFuture;
import com.hypixel.hytale.server.core.universe.world.storage.provider.IndexedStorageChunkStorageProvider;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.component.ResourceType;
import com.hypixel.hytale.server.core.universe.world.WorldConfigProvider;
import com.hypixel.hytale.server.core.universe.playerdata.PlayerStorage;
import com.hypixel.hytale.server.core.universe.world.World;
import java.util.UUID;
import java.nio.file.Path;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.metrics.MetricsRegistry;
import java.util.Map;
import javax.annotation.Nonnull;
import com.hypixel.hytale.common.plugin.PluginManifest;
import com.hypixel.hytale.metrics.MetricProvider;
import com.hypixel.hytale.server.core.receiver.IMessageReceiver;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;

public class Universe extends JavaPlugin implements IMessageReceiver, MetricProvider
{
    @Nonnull
    public static final PluginManifest MANIFEST;
    @Nonnull
    private static Map<Integer, String> LEGACY_BLOCK_ID_MAP;
    @Nonnull
    public static final MetricsRegistry<Universe> METRICS_REGISTRY;
    private static Universe instance;
    private ComponentType<EntityStore, PlayerRef> playerRefComponentType;
    @Nonnull
    private final Path path;
    @Nonnull
    private final Map<UUID, PlayerRef> players;
    @Nonnull
    private final Map<String, World> worlds;
    @Nonnull
    private final Map<UUID, World> worldsByUuid;
    @Nonnull
    private final Map<String, World> unmodifiableWorlds;
    private PlayerStorage playerStorage;
    private WorldConfigProvider worldConfigProvider;
    private ResourceType<ChunkStore, IndexedStorageChunkStorageProvider.IndexedStorageCache> indexedStorageCacheResourceType;
    private CompletableFuture<Void> universeReady;
    
    public static Universe get() {
        return Universe.instance;
    }
    
    public Universe(@Nonnull final JavaPluginInit init) {
        super(init);
        this.path = Constants.UNIVERSE_PATH;
        this.players = new ConcurrentHashMap<UUID, PlayerRef>();
        this.worlds = new ConcurrentHashMap<String, World>();
        this.worldsByUuid = new ConcurrentHashMap<UUID, World>();
        this.unmodifiableWorlds = Collections.unmodifiableMap((Map<? extends String, ? extends World>)this.worlds);
        Universe.instance = this;
        if (!Files.isDirectory(this.path, new LinkOption[0]) && !Options.getOptionSet().has(Options.BARE)) {
            try {
                Files.createDirectories(this.path, (FileAttribute<?>[])new FileAttribute[0]);
            }
            catch (final IOException e) {
                throw new RuntimeException("Failed to create universe directory", e);
            }
        }
        if (Options.getOptionSet().has(Options.BACKUP)) {
            final int frequencyMinutes = Math.max(Options.getOptionSet().valueOf(Options.BACKUP_FREQUENCY_MINUTES), 1);
            this.getLogger().at(Level.INFO).log("Scheduled backup to run every %d minute(s)", frequencyMinutes);
            HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> {
                try {
                    this.getLogger().at(Level.INFO).log("Backing up universe...");
                    this.runBackup().thenAccept(aVoid -> this.getLogger().at(Level.INFO).log("Completed scheduled backup."));
                }
                catch (final Exception e2) {
                    this.getLogger().at(Level.SEVERE).withCause(e2).log("Error backing up universe");
                }
            }, frequencyMinutes, frequencyMinutes, TimeUnit.MINUTES);
        }
    }
    
    @Nonnull
    public CompletableFuture<Void> runBackup() {
        return CompletableFuture.allOf((CompletableFuture<?>[])this.worlds.values().stream().map(world -> CompletableFuture.supplyAsync(() -> {
            final Store<ChunkStore> componentStore = world.getChunkStore().getStore();
            final ChunkSavingSystems.Data data = componentStore.getResource(ChunkStore.SAVE_RESOURCE);
            data.isSaving = false;
            return data;
        }, world).thenCompose((Function<? super ChunkSavingSystems.Data, ? extends CompletionStage<Object>>)ChunkSavingSystems.Data::waitForSavingChunks)).toArray(CompletableFuture[]::new)).thenCompose(aVoid -> BackupTask.start(this.path, Options.getOptionSet().valueOf(Options.BACKUP_DIRECTORY))).thenCompose(success -> CompletableFuture.allOf((CompletableFuture<?>[])this.worlds.values().stream().map(world -> CompletableFuture.runAsync(() -> {
            final Store<ChunkStore> componentStore2 = world.getChunkStore().getStore();
            final ChunkSavingSystems.Data data2 = componentStore2.getResource(ChunkStore.SAVE_RESOURCE);
            data2.isSaving = true;
        }, world)).toArray(CompletableFuture[]::new)).thenApply(aVoid -> success));
    }
    
    @Override
    protected void setup() {
        final EventRegistry eventRegistry = this.getEventRegistry();
        final ComponentRegistryProxy<ChunkStore> chunkStoreRegistry = this.getChunkStoreRegistry();
        final ComponentRegistryProxy<EntityStore> entityStoreRegistry = this.getEntityStoreRegistry();
        final CommandRegistry commandRegistry = this.getCommandRegistry();
        eventRegistry.register((short)(-48), ShutdownEvent.class, event -> this.disconnectAllPLayers());
        eventRegistry.register((short)(-32), ShutdownEvent.class, event -> this.shutdownAllWorlds());
        ISpawnProvider.CODEC.register("Global", (Class<?>)GlobalSpawnProvider.class, (C)GlobalSpawnProvider.CODEC);
        ISpawnProvider.CODEC.register("Individual", (Class<?>)IndividualSpawnProvider.class, (C)IndividualSpawnProvider.CODEC);
        ISpawnProvider.CODEC.register("FitToHeightMap", (Class<?>)FitToHeightMapSpawnProvider.class, (C)FitToHeightMapSpawnProvider.CODEC);
        IWorldGenProvider.CODEC.register("Flat", (Class<?>)FlatWorldGenProvider.class, (C)FlatWorldGenProvider.CODEC);
        IWorldGenProvider.CODEC.register("Dummy", (Class<?>)DummyWorldGenProvider.class, (C)DummyWorldGenProvider.CODEC);
        IWorldGenProvider.CODEC.register(Priority.DEFAULT, "Void", (Class<?>)VoidWorldGenProvider.class, VoidWorldGenProvider.CODEC);
        IWorldMapProvider.CODEC.register("Disabled", (Class<?>)DisabledWorldMapProvider.class, (C)DisabledWorldMapProvider.CODEC);
        IWorldMapProvider.CODEC.register(Priority.DEFAULT, "WorldGen", (Class<?>)WorldGenWorldMapProvider.class, WorldGenWorldMapProvider.CODEC);
        IChunkStorageProvider.CODEC.register(Priority.DEFAULT, "Hytale", (Class<?>)DefaultChunkStorageProvider.class, DefaultChunkStorageProvider.CODEC);
        IChunkStorageProvider.CODEC.register("Migration", (Class<?>)MigrationChunkStorageProvider.class, (C)MigrationChunkStorageProvider.CODEC);
        IChunkStorageProvider.CODEC.register("IndexedStorage", (Class<?>)IndexedStorageChunkStorageProvider.class, (C)IndexedStorageChunkStorageProvider.CODEC);
        IChunkStorageProvider.CODEC.register("Empty", (Class<?>)EmptyChunkStorageProvider.class, (C)EmptyChunkStorageProvider.CODEC);
        IResourceStorageProvider.CODEC.register(Priority.DEFAULT, "Hytale", (Class<?>)DefaultResourceStorageProvider.class, DefaultResourceStorageProvider.CODEC);
        IResourceStorageProvider.CODEC.register("Disk", (Class<?>)DiskResourceStorageProvider.class, (C)DiskResourceStorageProvider.CODEC);
        IResourceStorageProvider.CODEC.register("Empty", (Class<?>)EmptyResourceStorageProvider.class, (C)EmptyResourceStorageProvider.CODEC);
        this.indexedStorageCacheResourceType = chunkStoreRegistry.registerResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.class, IndexedStorageChunkStorageProvider.IndexedStorageCache::new);
        chunkStoreRegistry.registerSystem(new IndexedStorageChunkStorageProvider.IndexedStorageCacheSetupSystem());
        chunkStoreRegistry.registerSystem(new WorldPregenerateSystem());
        entityStoreRegistry.registerSystem(new WorldConfigSaveSystem());
        this.playerRefComponentType = entityStoreRegistry.registerComponent(PlayerRef.class, () -> {
            throw new UnsupportedOperationException();
        });
        entityStoreRegistry.registerSystem(new PlayerPingSystem());
        entityStoreRegistry.registerSystem(new PlayerConnectionFlushSystem(this.playerRefComponentType));
        entityStoreRegistry.registerSystem(new PlayerRefAddedSystem(this.playerRefComponentType));
        commandRegistry.registerCommand(new SetTickingCommand());
        commandRegistry.registerCommand(new BlockCommand());
        commandRegistry.registerCommand(new BlockSelectCommand());
        commandRegistry.registerCommand(new WorldCommand());
    }
    
    @Override
    protected void start() {
        final HytaleServerConfig config = HytaleServer.get().getConfig();
        if (config == null) {
            throw new IllegalStateException("Server config is not loaded!");
        }
        this.playerStorage = config.getPlayerStorageProvider().getPlayerStorage();
        final WorldConfigProvider.Default defaultConfigProvider = new WorldConfigProvider.Default();
        final PrepareUniverseEvent event = HytaleServer.get().getEventBus().dispatchFor((Class<? super PrepareUniverseEvent>)PrepareUniverseEvent.class).dispatch(new PrepareUniverseEvent(defaultConfigProvider));
        WorldConfigProvider worldConfigProvider = event.getWorldConfigProvider();
        if (worldConfigProvider == null) {
            worldConfigProvider = defaultConfigProvider;
        }
        this.worldConfigProvider = worldConfigProvider;
        try {
            final Path blockIdMapPath = this.path.resolve("blockIdMap.json");
            final Path path = this.path.resolve("blockIdMap.legacy.json");
            if (Files.isRegularFile(blockIdMapPath, new LinkOption[0])) {
                Files.move(blockIdMapPath, path, StandardCopyOption.REPLACE_EXISTING);
            }
            Files.deleteIfExists(this.path.resolve("blockIdMap.json.bak"));
            if (Files.isRegularFile(path, new LinkOption[0])) {
                final Map<Integer, String> map = new Int2ObjectOpenHashMap<String>();
                for (final BsonValue bsonValue : BsonUtil.readDocument(path).thenApply(document -> document.getArray("Blocks")).join()) {
                    final BsonDocument bsonDocument = bsonValue.asDocument();
                    map.put(bsonDocument.getNumber("Id").intValue(), bsonDocument.getString("BlockType").getValue());
                }
                Universe.LEGACY_BLOCK_ID_MAP = Collections.unmodifiableMap((Map<? extends Integer, ? extends String>)map);
            }
        }
        catch (final IOException e) {
            this.getLogger().at(Level.SEVERE).withCause(e).log("Failed to delete blockIdMap.json");
        }
        if (Options.getOptionSet().has(Options.BARE)) {
            this.universeReady = CompletableFuture.completedFuture((Void)null);
            HytaleServer.get().getEventBus().dispatch(AllWorldsLoadedEvent.class);
            return;
        }
        final ObjectArrayList<CompletableFuture<?>> loadingWorlds = new ObjectArrayList<CompletableFuture<?>>();
        try {
            final Path worldsPath = this.path.resolve("worlds");
            Files.createDirectories(worldsPath, (FileAttribute<?>[])new FileAttribute[0]);
            try (final DirectoryStream<Path> stream = Files.newDirectoryStream(worldsPath)) {
                for (final Path file : stream) {
                    if (HytaleServer.get().isShuttingDown()) {
                        if (stream != null) {
                            stream.close();
                        }
                        return;
                    }
                    if (file.equals(worldsPath)) {
                        continue;
                    }
                    if (!Files.isDirectory(file, new LinkOption[0])) {
                        continue;
                    }
                    final String name = file.getFileName().toString();
                    if (this.getWorld(name) == null) {
                        loadingWorlds.add(this.loadWorldFromStart(file, name).exceptionally(throwable -> {
                            this.getLogger().at(Level.SEVERE).withCause(throwable).log("Failed to load world: %s", name);
                            return null;
                        }));
                    }
                    else {
                        this.getLogger().at(Level.SEVERE).log("Skipping loading world '%s' because it already exists!", name);
                    }
                }
            }
            this.universeReady = CompletableFutureUtil._catch(CompletableFuture.allOf((CompletableFuture<?>[])loadingWorlds.toArray(CompletableFuture[]::new)).thenCompose(v -> {
                final String worldName = config.getDefaults().getWorld();
                if (worldName != null && !this.worlds.containsKey(worldName.toLowerCase())) {
                    return CompletableFutureUtil._catch(this.addWorld(worldName));
                }
                else {
                    return CompletableFuture.completedFuture((World)null);
                }
            }).thenRun(() -> HytaleServer.get().getEventBus().dispatch(AllWorldsLoadedEvent.class)));
        }
        catch (final IOException e2) {
            throw new RuntimeException("Failed to load Worlds", e2);
        }
    }
    
    @Override
    protected void shutdown() {
        this.disconnectAllPLayers();
        this.shutdownAllWorlds();
    }
    
    public void disconnectAllPLayers() {
        this.players.values().forEach(player -> player.getPacketHandler().disconnect("Stopping server!"));
    }
    
    public void shutdownAllWorlds() {
        final Iterator<World> iterator = this.worlds.values().iterator();
        while (iterator.hasNext()) {
            final World world = iterator.next();
            world.stop();
            iterator.remove();
        }
    }
    
    @Nonnull
    @Override
    public MetricResults toMetricResults() {
        return Universe.METRICS_REGISTRY.toMetricResults(this);
    }
    
    public CompletableFuture<Void> getUniverseReady() {
        return this.universeReady;
    }
    
    public ResourceType<ChunkStore, IndexedStorageChunkStorageProvider.IndexedStorageCache> getIndexedStorageCacheResourceType() {
        return this.indexedStorageCacheResourceType;
    }
    
    public boolean isWorldLoadable(@Nonnull final String name) {
        final Path savePath = this.path.resolve("worlds").resolve(name);
        return Files.isDirectory(savePath, new LinkOption[0]) && (Files.exists(savePath.resolve("config.bson"), new LinkOption[0]) || Files.exists(savePath.resolve("config.json"), new LinkOption[0]));
    }
    
    @Nonnull
    @CheckReturnValue
    public CompletableFuture<World> addWorld(@Nonnull final String name) {
        return this.addWorld(name, null, null);
    }
    
    @Nonnull
    @Deprecated
    @CheckReturnValue
    public CompletableFuture<World> addWorld(@Nonnull final String name, @Nullable final String generatorType, @Nullable final String chunkStorageType) {
        if (this.worlds.containsKey(name)) {
            throw new IllegalArgumentException("World " + name + " already exists!");
        }
        if (this.isWorldLoadable(name)) {
            throw new IllegalArgumentException("World " + name + " already exists on disk!");
        }
        final Path savePath = this.path.resolve("worlds").resolve(name);
        return this.worldConfigProvider.load(savePath, name).thenCompose(worldConfig -> {
            if (generatorType != null && !"default".equals(generatorType)) {
                final BuilderCodec<? extends IWorldGenProvider> providerCodec = IWorldGenProvider.CODEC.getCodecFor(generatorType);
                if (providerCodec == null) {
                    throw new IllegalArgumentException("Unknown generatorType '" + generatorType);
                }
                else {
                    final IWorldGenProvider provider = (IWorldGenProvider)providerCodec.getDefaultValue();
                    worldConfig.setWorldGenProvider(provider);
                    worldConfig.markChanged();
                }
            }
            if (chunkStorageType != null && !"default".equals(chunkStorageType)) {
                final BuilderCodec<? extends IChunkStorageProvider> providerCodec2 = IChunkStorageProvider.CODEC.getCodecFor(chunkStorageType);
                if (providerCodec2 == null) {
                    throw new IllegalArgumentException("Unknown chunkStorageType '" + chunkStorageType);
                }
                else {
                    final IChunkStorageProvider provider2 = (IChunkStorageProvider)providerCodec2.getDefaultValue();
                    worldConfig.setChunkStorageProvider(provider2);
                    worldConfig.markChanged();
                }
            }
            return this.makeWorld(name, savePath, worldConfig);
        });
    }
    
    @Nonnull
    @CheckReturnValue
    public CompletableFuture<World> makeWorld(@Nonnull final String name, @Nonnull final Path savePath, @Nonnull final WorldConfig worldConfig) {
        return this.makeWorld(name, savePath, worldConfig, true);
    }
    
    @Nonnull
    @CheckReturnValue
    public CompletableFuture<World> makeWorld(@Nonnull final String name, @Nonnull final Path savePath, @Nonnull final WorldConfig worldConfig, final boolean start) {
        final Map<PluginIdentifier, SemverRange> map = worldConfig.getRequiredPlugins();
        if (map != null) {
            final PluginManager pluginManager = PluginManager.get();
            for (final Map.Entry<PluginIdentifier, SemverRange> entry : map.entrySet()) {
                if (!pluginManager.hasPlugin(entry.getKey(), entry.getValue())) {
                    this.getLogger().at(Level.SEVERE).log("Failed to load world! Missing plugin: %s, Version: %s", entry.getKey(), entry.getValue());
                    throw new IllegalStateException("Missing plugin");
                }
            }
        }
        if (this.worlds.containsKey(name)) {
            throw new IllegalArgumentException("World " + name + " already exists!");
        }
        return CompletableFuture.supplyAsync((Supplier<Object>)SneakyThrow.sneakySupplier(() -> {
            final World world = new World(name, savePath, worldConfig);
            final AddWorldEvent event = HytaleServer.get().getEventBus().dispatchFor((Class<? super AddWorldEvent>)AddWorldEvent.class, name).dispatch(new AddWorldEvent(world));
            if (!event.isCancelled() && !HytaleServer.get().isShuttingDown()) {
                final World oldWorldByName = this.worlds.putIfAbsent(name.toLowerCase(), world);
                if (oldWorldByName != null) {
                    throw new ConcurrentModificationException("World with name " + name + " already exists but didn't before! Looks like you have a race condition.");
                }
                else {
                    final World oldWorldByUuid = this.worldsByUuid.putIfAbsent(worldConfig.getUuid(), world);
                    if (oldWorldByUuid != null) {
                        throw new ConcurrentModificationException("World with UUID " + String.valueOf(worldConfig.getUuid()) + " already exists but didn't before! Looks like you have a race condition.");
                    }
                    else {
                        return world;
                    }
                }
            }
            else {
                throw new WorldLoadCancelledException();
            }
        })).thenCompose((Function<? super Object, ? extends CompletionStage<Object>>)World::init).thenCompose(world -> {
            if (!Options.getOptionSet().has(Options.MIGRATIONS) && start) {
                return world.start().thenApply(v -> world);
            }
            else {
                return (CompletableFuture<Object>)CompletableFuture.completedFuture(world);
            }
        }).whenComplete((world, throwable) -> {
            if (throwable != null) {
                final String nameLower = name.toLowerCase();
                if (this.worlds.containsKey(nameLower)) {
                    try {
                        this.removeWorldExceptionally(name);
                    }
                    catch (final Exception e) {
                        this.getLogger().at(Level.WARNING).withCause(e).log("Failed to clean up world '%s' after init failure", name);
                    }
                }
            }
        });
    }
    
    private CompletableFuture<Void> loadWorldFromStart(@Nonnull final Path savePath, @Nonnull final String name) {
        return this.worldConfigProvider.load(savePath, name).thenCompose(worldConfig -> {
            if (worldConfig.isDeleteOnUniverseStart()) {
                return CompletableFuture.runAsync(() -> {
                    try {
                        FileUtil.deleteDirectory(savePath);
                        this.getLogger().at(Level.INFO).log("Deleted world " + name + " from DeleteOnUniverseStart flag on universe start at " + String.valueOf(savePath));
                    }
                    catch (final Throwable t) {
                        throw new RuntimeException("Error deleting world directory on universe start", t);
                    }
                });
            }
            else {
                return this.makeWorld(name, savePath, worldConfig).thenApply(x -> null);
            }
        });
    }
    
    @Nonnull
    @CheckReturnValue
    public CompletableFuture<World> loadWorld(@Nonnull final String name) {
        if (this.worlds.containsKey(name)) {
            throw new IllegalArgumentException("World " + name + " already loaded!");
        }
        final Path savePath = this.path.resolve("worlds").resolve(name);
        if (!Files.isDirectory(savePath, new LinkOption[0])) {
            throw new IllegalArgumentException("World " + name + " does not exist!");
        }
        return this.worldConfigProvider.load(savePath, name).thenCompose(worldConfig -> this.makeWorld(name, savePath, worldConfig));
    }
    
    @Nullable
    public World getWorld(@Nullable final String worldName) {
        if (worldName == null) {
            return null;
        }
        return this.worlds.get(worldName.toLowerCase());
    }
    
    @Nullable
    public World getWorld(@Nonnull final UUID uuid) {
        return this.worldsByUuid.get(uuid);
    }
    
    @Nullable
    public World getDefaultWorld() {
        final HytaleServerConfig config = HytaleServer.get().getConfig();
        if (config == null) {
            return null;
        }
        final String worldName = config.getDefaults().getWorld();
        return (worldName != null) ? this.getWorld(worldName) : null;
    }
    
    public boolean removeWorld(@Nonnull final String name) {
        Objects.requireNonNull(name, "Name can't be null!");
        final String nameLower = name.toLowerCase();
        final World world = this.worlds.get(nameLower);
        if (world == null) {
            throw new NullPointerException("World " + name + " doesn't exist!");
        }
        final RemoveWorldEvent event = HytaleServer.get().getEventBus().dispatchFor((Class<? super RemoveWorldEvent>)RemoveWorldEvent.class, name).dispatch(new RemoveWorldEvent(world, RemoveWorldEvent.RemovalReason.GENERAL));
        if (event.isCancelled()) {
            return false;
        }
        this.worlds.remove(nameLower);
        this.worldsByUuid.remove(world.getWorldConfig().getUuid());
        if (world.isAlive()) {
            world.stopIndividualWorld();
        }
        world.validateDeleteOnRemove();
        return true;
    }
    
    public void removeWorldExceptionally(@Nonnull final String name) {
        Objects.requireNonNull(name, "Name can't be null!");
        this.getLogger().at(Level.INFO).log("Removing world exceptionally: %s", name);
        final String nameLower = name.toLowerCase();
        final World world = this.worlds.get(nameLower);
        if (world == null) {
            throw new NullPointerException("World " + name + " doesn't exist!");
        }
        HytaleServer.get().getEventBus().dispatchFor((Class<? super IEvent<String>>)RemoveWorldEvent.class, name).dispatch(new RemoveWorldEvent(world, RemoveWorldEvent.RemovalReason.EXCEPTIONAL));
        this.worlds.remove(nameLower);
        this.worldsByUuid.remove(world.getWorldConfig().getUuid());
        if (world.isAlive()) {
            world.stopIndividualWorld();
        }
        world.validateDeleteOnRemove();
    }
    
    @Nonnull
    public Path getPath() {
        return this.path;
    }
    
    @Nonnull
    public Map<String, World> getWorlds() {
        return this.unmodifiableWorlds;
    }
    
    @Nonnull
    public List<PlayerRef> getPlayers() {
        return new ObjectArrayList<PlayerRef>(this.players.values());
    }
    
    @Nullable
    public PlayerRef getPlayer(@Nonnull final UUID uuid) {
        return this.players.get(uuid);
    }
    
    @Nullable
    public PlayerRef getPlayer(@Nonnull final String value, @Nonnull final NameMatching matching) {
        return matching.find(this.players.values(), value, v -> v.getComponent(PlayerRef.getComponentType()).getUsername());
    }
    
    @Nullable
    public PlayerRef getPlayer(@Nonnull final String value, @Nonnull final Comparator<String> comparator, @Nonnull final BiPredicate<String, String> equality) {
        return NameMatching.find(this.players.values(), value, v -> v.getComponent(PlayerRef.getComponentType()).getUsername(), comparator, equality);
    }
    
    @Nullable
    public PlayerRef getPlayerByUsername(@Nonnull final String value, @Nonnull final NameMatching matching) {
        return matching.find(this.players.values(), value, PlayerRef::getUsername);
    }
    
    @Nullable
    public PlayerRef getPlayerByUsername(@Nonnull final String value, @Nonnull final Comparator<String> comparator, @Nonnull final BiPredicate<String, String> equality) {
        return NameMatching.find(this.players.values(), value, PlayerRef::getUsername, comparator, equality);
    }
    
    public int getPlayerCount() {
        return this.players.size();
    }
    
    @Nonnull
    public CompletableFuture<PlayerRef> addPlayer(@Nonnull final Channel channel, @Nonnull final String language, @Nonnull final ProtocolVersion protocolVersion, @Nonnull final UUID uuid, @Nonnull final String username, @Nonnull final PlayerAuthentication auth, final int clientViewRadiusChunks, @Nullable final PlayerSkin skin) {
        final GamePacketHandler playerConnection = new GamePacketHandler(channel, protocolVersion, auth);
        playerConnection.setQueuePackets(false);
        this.getLogger().at(Level.INFO).log("Adding player '%s (%s)", username, uuid);
        return this.playerStorage.load(uuid).exceptionally(throwable -> {
            throw new RuntimeException("Exception when adding player to universe:", throwable);
        }).thenCompose(holder -> {
            final ChunkTracker chunkTrackerComponent = new ChunkTracker();
            final PlayerRef playerRefComponent = new PlayerRef(holder, uuid, username, language, playerConnection, chunkTrackerComponent);
            chunkTrackerComponent.setDefaultMaxChunksPerSecond(playerRefComponent);
            holder.putComponent(PlayerRef.getComponentType(), playerRefComponent);
            holder.putComponent(ChunkTracker.getComponentType(), chunkTrackerComponent);
            holder.putComponent(UUIDComponent.getComponentType(), new UUIDComponent(uuid));
            holder.ensureComponent(PositionDataComponent.getComponentType());
            holder.ensureComponent(MovementAudioComponent.getComponentType());
            final Player playerComponent = holder.ensureAndGetComponent(Player.getComponentType());
            playerComponent.init(uuid, playerRefComponent);
            final PlayerConfigData playerConfig = playerComponent.getPlayerConfigData();
            playerConfig.cleanup(this);
            PacketHandler.logConnectionTimings(channel, "Load Player Config", Level.FINEST);
            if (skin != null) {
                holder.putComponent(PlayerSkinComponent.getComponentType(), new PlayerSkinComponent(skin));
                holder.putComponent(ModelComponent.getComponentType(), new ModelComponent(CosmeticsModule.get().createModel(skin)));
            }
            playerConnection.setPlayerRef(playerRefComponent, playerComponent);
            NettyUtil.setChannelHandler(channel, playerConnection);
            playerComponent.setClientViewRadius(clientViewRadiusChunks);
            final EntityTrackerSystems.EntityViewer entityViewerComponent = holder.getComponent(EntityTrackerSystems.EntityViewer.getComponentType());
            if (entityViewerComponent != null) {
                entityViewerComponent.viewRadiusBlocks = playerComponent.getViewRadius() * 32;
            }
            else {
                final EntityTrackerSystems.EntityViewer entityViewerComponent2 = new EntityTrackerSystems.EntityViewer(playerComponent.getViewRadius() * 32, playerConnection);
                holder.addComponent(EntityTrackerSystems.EntityViewer.getComponentType(), entityViewerComponent2);
            }
            final PlayerRef existingPlayer = this.players.putIfAbsent(uuid, playerRefComponent);
            if (existingPlayer != null) {
                this.getLogger().at(Level.WARNING).log("Player '%s' (%s) already joining from another connection, rejecting duplicate", username, uuid);
                playerConnection.disconnect("A connection with this account is already in progress");
                return CompletableFuture.completedFuture((Object)null);
            }
            else {
                final String lastWorldName = playerConfig.getWorld();
                final World lastWorld = this.getWorld(lastWorldName);
                HytaleServer.get().getEventBus().dispatchFor((Class<? super IEvent>)PlayerConnectEvent.class);
                new PlayerConnectEvent(holder, playerRefComponent, (lastWorld != null) ? lastWorld : this.getDefaultWorld());
                final PlayerConnectEvent playerConnectEvent;
                final IEventDispatcher<PlayerConnectEvent, PlayerConnectEvent> eventDispatcher;
                final PlayerConnectEvent event = eventDispatcher.dispatch(playerConnectEvent);
                final World world = (event.getWorld() != null) ? event.getWorld() : this.getDefaultWorld();
                if (world == null) {
                    this.players.remove(uuid, playerRefComponent);
                    playerConnection.disconnect("No world available to join");
                    this.getLogger().at(Level.SEVERE).log("Player '%s' (%s) could not join - no default world configured", username, uuid);
                    return CompletableFuture.completedFuture((Object)null);
                }
                else {
                    if (lastWorldName != null && lastWorld == null) {
                        playerComponent.sendMessage(Message.translation("server.universe.failedToFindWorld").param("lastWorldName", lastWorldName).param("name", world.getName()));
                    }
                    PacketHandler.logConnectionTimings(channel, "Processed Referral", Level.FINEST);
                    playerRefComponent.getPacketHandler().write(new ServerTags(AssetRegistry.getClientTags()));
                    return world.addPlayer(playerRefComponent, null, false, false).thenApply(p -> {
                        PacketHandler.logConnectionTimings(channel, "Add to World", Level.FINEST);
                        if (!channel.isActive()) {
                            if (p != null) {
                                playerComponent.remove();
                            }
                            this.players.remove(uuid, playerRefComponent);
                            this.getLogger().at(Level.WARNING).log("Player '%s' (%s) disconnected during world join, cleaned up from universe", username, uuid);
                            return null;
                        }
                        else if (playerComponent.wasRemoved()) {
                            this.players.remove(uuid, playerRefComponent);
                            return null;
                        }
                        else {
                            return p;
                        }
                    }).exceptionally(throwable -> {
                        this.players.remove(uuid, playerRefComponent);
                        playerComponent.remove();
                        throw new RuntimeException("Exception when adding player to universe:", throwable);
                    });
                }
            }
        });
    }
    
    public void removePlayer(@Nonnull final PlayerRef playerRef) {
        this.getLogger().at(Level.INFO).log("Removing player '" + playerRef.getUsername() + "' (" + String.valueOf(playerRef.getUuid()));
        final IEventDispatcher<PlayerDisconnectEvent, PlayerDisconnectEvent> eventDispatcher = HytaleServer.get().getEventBus().dispatchFor((Class<? super PlayerDisconnectEvent>)PlayerDisconnectEvent.class);
        if (eventDispatcher.hasListener()) {
            eventDispatcher.dispatch(new PlayerDisconnectEvent(playerRef));
        }
        final Ref<EntityStore> ref = playerRef.getReference();
        if (ref == null) {
            this.finalizePlayerRemoval(playerRef);
            return;
        }
        final World world = ref.getStore().getExternalData().getWorld();
        if (world.isInThread()) {
            final Player playerComponent = ref.getStore().getComponent(ref, Player.getComponentType());
            if (playerComponent != null) {
                playerComponent.remove();
            }
            this.finalizePlayerRemoval(playerRef);
        }
        else {
            final CompletableFuture<Void> removedFuture = new CompletableFuture<Void>();
            CompletableFuture.runAsync(() -> {
                final Player playerComponent2 = ref.getStore().getComponent(ref, Player.getComponentType());
                if (playerComponent2 != null) {
                    playerComponent2.remove();
                }
                return;
            }, world).whenComplete((unused, throwable) -> {
                if (throwable != null) {
                    removedFuture.completeExceptionally(throwable);
                }
                else {
                    removedFuture.complete(unused);
                }
                return;
            });
            removedFuture.orTimeout(5L, TimeUnit.SECONDS).whenComplete((result, error) -> {
                if (error != null) {
                    this.getLogger().at(Level.WARNING).withCause(error).log("Timeout or error waiting for player '%s' removal from world store", playerRef.getUsername());
                }
                this.finalizePlayerRemoval(playerRef);
            });
        }
    }
    
    private void finalizePlayerRemoval(@Nonnull final PlayerRef playerRef) {
        this.players.remove(playerRef.getUuid());
        if (Constants.SINGLEPLAYER) {
            if (this.players.isEmpty()) {
                this.getLogger().at(Level.INFO).log("No players left on singleplayer server shutting down!");
                HytaleServer.get().shutdownServer();
            }
            else if (SingleplayerModule.isOwner(playerRef)) {
                this.getLogger().at(Level.INFO).log("Owner left the singleplayer server shutting down!");
                this.getPlayers().forEach(p -> p.getPacketHandler().disconnect(playerRef.getUsername() + " left! Shutting down singleplayer world!"));
                HytaleServer.get().shutdownServer();
            }
        }
    }
    
    @Nonnull
    public CompletableFuture<PlayerRef> resetPlayer(@Nonnull final PlayerRef oldPlayer) {
        return this.playerStorage.load(oldPlayer.getUuid()).exceptionally(throwable -> {
            throw new RuntimeException("Exception when adding player to universe:", throwable);
        }).thenCompose(holder -> this.resetPlayer(oldPlayer, holder));
    }
    
    @Nonnull
    public CompletableFuture<PlayerRef> resetPlayer(@Nonnull final PlayerRef oldPlayer, @Nonnull final Holder<EntityStore> holder) {
        return this.resetPlayer(oldPlayer, holder, null, null);
    }
    
    @Nonnull
    public CompletableFuture<PlayerRef> resetPlayer(@Nonnull final PlayerRef playerRef, @Nonnull final Holder<EntityStore> holder, @Nullable final World world, @Nullable final Transform transform) {
        final UUID uuid = playerRef.getUuid();
        final Player oldPlayer = playerRef.getComponent(Player.getComponentType());
        World targetWorld;
        if (world == null) {
            targetWorld = oldPlayer.getWorld();
        }
        else {
            targetWorld = world;
        }
        this.getLogger().at(Level.INFO).log("Resetting player '%s', moving to world '%s' at location %s (%s)", playerRef.getUsername(), (world != null) ? world.getName() : null, transform, playerRef.getUuid());
        final GamePacketHandler playerConnection = (GamePacketHandler)playerRef.getPacketHandler();
        final Player newPlayer = holder.ensureAndGetComponent(Player.getComponentType());
        newPlayer.init(uuid, playerRef);
        final CompletableFuture<Void> leaveWorld = new CompletableFuture<Void>();
        if (oldPlayer.getWorld() != null) {
            oldPlayer.getWorld().execute(() -> {
                playerRef.removeFromStore();
                leaveWorld.complete(null);
                return;
            });
        }
        else {
            leaveWorld.complete(null);
        }
        return leaveWorld.thenAccept(v -> {
            oldPlayer.resetManagers(holder);
            newPlayer.copyFrom(oldPlayer);
            final EntityTrackerSystems.EntityViewer viewer = holder.getComponent(EntityTrackerSystems.EntityViewer.getComponentType());
            if (viewer != null) {
                viewer.viewRadiusBlocks = newPlayer.getViewRadius() * 32;
            }
            else {
                final EntityTrackerSystems.EntityViewer viewer2 = new EntityTrackerSystems.EntityViewer(newPlayer.getViewRadius() * 32, playerConnection);
                holder.addComponent(EntityTrackerSystems.EntityViewer.getComponentType(), viewer2);
            }
            playerConnection.setPlayerRef(playerRef, newPlayer);
            playerRef.replaceHolder(holder);
            holder.putComponent(PlayerRef.getComponentType(), playerRef);
        }).thenCompose(v -> targetWorld.addPlayer(playerRef, transform));
    }
    
    @Override
    public void sendMessage(@Nonnull final Message message) {
        for (final PlayerRef ref : this.players.values()) {
            ref.sendMessage(message);
        }
    }
    
    public void broadcastPacket(@Nonnull final Packet packet) {
        for (final PlayerRef player : this.players.values()) {
            player.getPacketHandler().write(packet);
        }
    }
    
    public void broadcastPacketNoCache(@Nonnull final Packet packet) {
        for (final PlayerRef player : this.players.values()) {
            player.getPacketHandler().writeNoCache(packet);
        }
    }
    
    public void broadcastPacket(@Nonnull final Packet... packets) {
        for (final PlayerRef player : this.players.values()) {
            player.getPacketHandler().write(packets);
        }
    }
    
    public PlayerStorage getPlayerStorage() {
        return this.playerStorage;
    }
    
    public void setPlayerStorage(@Nonnull final PlayerStorage playerStorage) {
        this.playerStorage = playerStorage;
    }
    
    public WorldConfigProvider getWorldConfigProvider() {
        return this.worldConfigProvider;
    }
    
    @Nonnull
    public ComponentType<EntityStore, PlayerRef> getPlayerRefComponentType() {
        return this.playerRefComponentType;
    }
    
    @Nonnull
    @Deprecated
    public static Map<Integer, String> getLegacyBlockIdMap() {
        return Universe.LEGACY_BLOCK_ID_MAP;
    }
    
    public static Path getWorldGenPath() {
        final OptionSet optionSet = Options.getOptionSet();
        Path worldGenPath;
        if (optionSet.has(Options.WORLD_GEN_DIRECTORY)) {
            worldGenPath = optionSet.valueOf(Options.WORLD_GEN_DIRECTORY);
        }
        else {
            worldGenPath = AssetUtil.getHytaleAssetsPath().resolve("Server").resolve("World");
        }
        return worldGenPath;
    }
    
    static {
        MANIFEST = PluginManifest.corePlugin(Universe.class).build();
        Universe.LEGACY_BLOCK_ID_MAP = Collections.emptyMap();
        METRICS_REGISTRY = new MetricsRegistry<Universe>().register("Worlds", universe -> universe.getWorlds().values().toArray(World[]::new), (Codec<World[]>)new ArrayCodec<World>(World.METRICS_REGISTRY, World[]::new)).register("PlayerCount", Universe::getPlayerCount, Codec.INTEGER);
    }
}
