package com.hypixel.hytale.server.core;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.common.plugin.PluginManifest;
import com.hypixel.hytale.common.thread.HytaleForkJoinThreadFactory;
import com.hypixel.hytale.common.util.FormatUtil;
import com.hypixel.hytale.common.util.GCUtil;
import com.hypixel.hytale.common.util.HardwareUtil;
import com.hypixel.hytale.common.util.NetworkUtil;
import com.hypixel.hytale.common.util.java.ManifestUtil;
import com.hypixel.hytale.event.EventBus;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.logger.backend.HytaleLogManager;
import com.hypixel.hytale.logger.backend.HytaleLoggerBackend;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.math.util.TrigMathUtil;
import com.hypixel.hytale.metrics.JVMMetrics;
import com.hypixel.hytale.metrics.MetricsRegistry;
import com.hypixel.hytale.plugin.early.EarlyPluginLoader;
import com.hypixel.hytale.server.core.asset.AssetRegistryLoader;
import com.hypixel.hytale.server.core.asset.LoadAssetEvent;
import com.hypixel.hytale.server.core.auth.ServerAuthManager;
import com.hypixel.hytale.server.core.auth.SessionServiceClient;
import com.hypixel.hytale.server.core.command.system.CommandManager;
import com.hypixel.hytale.server.core.console.ConsoleSender;
import com.hypixel.hytale.server.core.event.events.BootEvent;
import com.hypixel.hytale.server.core.event.events.ShutdownEvent;
import com.hypixel.hytale.server.core.io.ServerManager;
import com.hypixel.hytale.server.core.io.netty.NettyUtil;
import com.hypixel.hytale.server.core.modules.singleplayer.SingleplayerModule;
import com.hypixel.hytale.server.core.plugin.PluginBase;
import com.hypixel.hytale.server.core.plugin.PluginClassLoader;
import com.hypixel.hytale.server.core.plugin.PluginManager;
import com.hypixel.hytale.server.core.plugin.PluginState;
import com.hypixel.hytale.server.core.universe.Universe;
import com.hypixel.hytale.server.core.universe.datastore.DataStoreProvider;
import com.hypixel.hytale.server.core.universe.datastore.DiskDataStoreProvider;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.util.concurrent.ThreadUtil;
import io.netty.handler.codec.quic.Quic;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryOptions;
import io.sentry.protocol.Contexts;
import io.sentry.protocol.User;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import joptsimple.OptionSet;
public class HytaleServer {
public static final int DEFAULT_PORT = 5520;
public static final ScheduledExecutorService SCHEDULED_EXECUTOR = Executors.newSingleThreadScheduledExecutor(ThreadUtil.daemon("Scheduler"));
@Nonnull
public static final MetricsRegistry<HytaleServer> METRICS_REGISTRY;
private static final HytaleLogger LOGGER;
private static HytaleServer instance;
private final Semaphore aliveLock = new Semaphore(0);
private final AtomicBoolean booting = new AtomicBoolean(false);
private final AtomicBoolean booted = new AtomicBoolean(false);
private final AtomicReference<ShutdownReason> shutdown = new AtomicReference();
private final EventBus eventBus;
private final PluginManager pluginManager;
private final CommandManager commandManager;
@Nonnull
private final HytaleServerConfig hytaleServerConfig;
private final Instant boot;
private final long bootStart;
private int pluginsProgress;
public HytaleServer() throws IOException {
this.eventBus = new EventBus(Options.getOptionSet().has(Options.EVENT_DEBUG));
this.pluginManager = new PluginManager();
this.commandManager = new CommandManager();
instance = this;
Quic.ensureAvailability();
HytaleLoggerBackend.setIndent(25);
ThreadUtil.forceTimeHighResolution();
ThreadUtil.createKeepAliveThread(this.aliveLock);
this.boot = Instant.now();
this.bootStart = System.nanoTime();
LOGGER.at(Level.INFO).log("Starting HytaleServer");
Constants.init();
DataStoreProvider.CODEC.register("Disk", DiskDataStoreProvider.class, DiskDataStoreProvider.CODEC);
LOGGER.at(Level.INFO).log("Loading config...");
this.hytaleServerConfig = HytaleServerConfig.load();
HytaleLoggerBackend.reloadLogLevels();
System.setProperty("java.util.concurrent.ForkJoinPool.common.threadFactory", HytaleForkJoinThreadFactory.class.getName());
OptionSet optionSet = Options.getOptionSet();
LOGGER.at(Level.INFO).log("Authentication mode: %s", optionSet.valueOf(Options.AUTH_MODE));
ServerAuthManager.getInstance().initialize();
if (EarlyPluginLoader.hasTransformers()) {
HytaleLogger.getLogger().at(Level.INFO).log("Early plugins loaded!! Disabling Sentry!!");
} else if (!optionSet.has(Options.DISABLE_SENTRY)) {
LOGGER.at(Level.INFO).log("Enabling Sentry");
SentryOptions options = new SentryOptions();
options.setDsn("https://6043a13c7b5c45b5c834b6d896fb378e@sentry.hytale.com/4");
options.setRelease(ManifestUtil.getImplementationVersion());
options.setDist(ManifestUtil.getImplementationRevisionId());
options.setEnvironment("release");
options.setTag("patchline", ManifestUtil.getPatchline());
options.setServerName(NetworkUtil.getHostName());
options.setBeforeSend((event, hint) -> {
Throwable throwable = event.getThrowable();
if (PluginClassLoader.isFromThirdPartyPlugin(throwable)) {
return null;
} else {
Contexts contexts = event.getContexts();
HashMap<String, Object> serverContext = new HashMap();
serverContext.put("name", this.getServerName());
serverContext.put("max-players", this.getConfig().getMaxPlayers());
ServerManager serverManager = ServerManager.get();
if (serverManager != null) {
serverContext.put("listeners", serverManager.getListeners().stream().map(Object::toString).toList());
}
contexts.put("server", serverContext);
Universe universe = Universe.get();
if (universe != null) {
HashMap<String, Object> universeContext = new HashMap();
universeContext.put("path", universe.getPath().toString());
universeContext.put("player-count", universe.getPlayerCount());
universeContext.put("worlds", universe.getWorlds().keySet().stream().toList());
contexts.put("universe", universeContext);
}
HashMap<String, Object> pluginsContext = new HashMap();
for(PluginBase plugin : this.pluginManager.getPlugins()) {
PluginManifest manifest = plugin.getManifest();
HashMap<String, Object> pluginInfo = new HashMap();
pluginInfo.put("version", manifest.getVersion().toString());
pluginInfo.put("state", plugin.getState().name());
pluginsContext.put(plugin.getIdentifier().toString(), pluginInfo);
}
contexts.put("plugins", pluginsContext);
User user = new User();
HashMap<String, Object> unknown = new HashMap();
user.setUnknown(unknown);
UUID hardwareUUID = HardwareUtil.getUUID();
if (hardwareUUID != null) {
unknown.put("hardware-uuid", hardwareUUID.toString());
}
ServerAuthManager authManager = ServerAuthManager.getInstance();
unknown.put("auth-mode", authManager.getAuthMode().toString());
SessionServiceClient.GameProfile profile = authManager.getSelectedProfile();
if (profile != null) {
user.setUsername(profile.username);
user.setId(profile.uuid.toString());
}
user.setIpAddress("{{auto}}");
event.setUser(user);
return event;
}
});
Sentry.init(options);
Sentry.configureScope((scope) -> {
UUID hardwareUUID = HardwareUtil.getUUID();
if (hardwareUUID != null) {
scope.setContexts("hardware", (Object)Map.of("uuid", hardwareUUID.toString()));
}
scope.setContexts("build", (Object)Map.of("version", String.valueOf(ManifestUtil.getImplementationVersion()), "revision-id", String.valueOf(ManifestUtil.getImplementationRevisionId()), "patchline", String.valueOf(ManifestUtil.getPatchline()), "environment", "release"));
if (Constants.SINGLEPLAYER) {
scope.setContexts("singleplayer", (Object)Map.of("owner-uuid", String.valueOf(SingleplayerModule.getUuid()), "owner-name", SingleplayerModule.getUsername()));
}
});
HytaleLogger.getLogger().setSentryClient(Sentry.getCurrentScopes());
}
NettyUtil.init();
float sin = TrigMathUtil.sin(0.0F);
float atan2 = TrigMathUtil.atan2(0.0F, 0.0F);
Thread shutdownHook = new Thread(() -> {
if (this.shutdown.getAndSet(ShutdownReason.SIGINT) == null) {
this.shutdown0(ShutdownReason.SIGINT);
}
}, "ShutdownHook");
shutdownHook.setDaemon(false);
Runtime.getRuntime().addShutdownHook(shutdownHook);
AssetRegistryLoader.init();
for(PluginManifest manifest : Constants.CORE_PLUGINS) {
this.pluginManager.registerCorePlugin(manifest);
}
GCUtil.register((info) -> {
Universe universe = Universe.get();
if (universe != null) {
for(World world : universe.getWorlds().values()) {
world.markGCHasRun();
}
}
});
this.boot();
}
@Nonnull
public EventBus getEventBus() {
return this.eventBus;
}
@Nonnull
public PluginManager getPluginManager() {
return this.pluginManager;
}
@Nonnull
public CommandManager getCommandManager() {
return this.commandManager;
}
@Nonnull
public HytaleServerConfig getConfig() {
return this.hytaleServerConfig;
}
private void boot() {
if (!this.booting.getAndSet(true)) {
HytaleLogger.Api var10000 = LOGGER.at(Level.INFO);
String var10001 = ManifestUtil.getImplementationVersion();
var10000.log("Booting up HytaleServer - Version: " + var10001 + ", Revision: " + ManifestUtil.getImplementationRevisionId());
try {
this.pluginsProgress = 0;
this.sendSingleplayerProgress();
if (this.isShuttingDown()) {
return;
}
LOGGER.at(Level.INFO).log("Setup phase...");
this.commandManager.registerCommands();
this.pluginManager.setup();
ServerAuthManager.getInstance().initializeCredentialStore();
LOGGER.at(Level.INFO).log("Setup phase completed! Boot time %s", FormatUtil.nanosToString(System.nanoTime() - this.bootStart));
if (this.isShuttingDown()) {
return;
}
LoadAssetEvent loadAssetEvent = (LoadAssetEvent)get().getEventBus().dispatchFor(LoadAssetEvent.class).dispatch(new LoadAssetEvent(this.bootStart));
if (this.isShuttingDown()) {
return;
}
if (loadAssetEvent.isShouldShutdown()) {
List<String> reasons = loadAssetEvent.getReasons();
String join = String.join(", ", reasons);
LOGGER.at(Level.SEVERE).log("Asset validation FAILED with %d reason(s): %s", reasons.size(), join);
this.shutdownServer(ShutdownReason.VALIDATE_ERROR.withMessage(join));
return;
}
if (Options.getOptionSet().has(Options.SHUTDOWN_AFTER_VALIDATE)) {
LOGGER.at(Level.INFO).log("Asset validation passed");
this.shutdownServer(ShutdownReason.SHUTDOWN);
return;
}
this.pluginsProgress = 0;
this.sendSingleplayerProgress();
if (this.isShuttingDown()) {
return;
}
LOGGER.at(Level.INFO).log("Starting plugin manager...");
this.pluginManager.start();
LOGGER.at(Level.INFO).log("Plugin manager started! Startup time so far: %s", FormatUtil.nanosToString(System.nanoTime() - this.bootStart));
if (this.isShuttingDown()) {
return;
}
this.sendSingleplayerSignal("-=|Enabled|0");
} catch (Throwable throwable) {
((HytaleLogger.Api)LOGGER.at(Level.SEVERE).withCause(throwable)).log("Failed to boot HytaleServer!");
Throwable t;
for(t = throwable; t.getCause() != null; t = t.getCause()) {
}
this.shutdownServer(ShutdownReason.CRASH.withMessage("Failed to start server! " + t.getMessage()));
}
if (this.hytaleServerConfig.consumeHasChanged()) {
HytaleServerConfig.save(this.hytaleServerConfig).join();
}
SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> {
try {
if (this.hytaleServerConfig.consumeHasChanged()) {
HytaleServerConfig.save(this.hytaleServerConfig).join();
}
} catch (Exception e) {
((HytaleLogger.Api)LOGGER.at(Level.SEVERE).withCause(e)).log("Failed to save server config!");
}
}, 1L, 1L, TimeUnit.MINUTES);
LOGGER.at(Level.INFO).log("Getting Hytale Universe ready...");
Universe.get().getUniverseReady().join();
LOGGER.at(Level.INFO).log("Universe ready!");
List<String> tags = new ObjectArrayList<String>();
if (Constants.SINGLEPLAYER) {
tags.add("Singleplayer");
} else {
tags.add("Multiplayer");
}
if (Constants.FRESH_UNIVERSE) {
tags.add("Fresh Universe");
}
this.booted.set(true);
ServerManager.get().waitForBindComplete();
this.eventBus.dispatch(BootEvent.class);
List<String> bootCommands = Options.getOptionSet().valuesOf(Options.BOOT_COMMAND);
if (!bootCommands.isEmpty()) {
CommandManager.get().handleCommands(ConsoleSender.INSTANCE, new ArrayDeque(bootCommands)).join();
}
String border = "\u001b[0;32m===============================================================================================";
LOGGER.at(Level.INFO).log("\u001b[0;32m===============================================================================================");
LOGGER.at(Level.INFO).log("%s Hytale Server Booted! [%s] took %s", "\u001b[0;32m", String.join(", ", tags), FormatUtil.nanosToString(System.nanoTime() - this.bootStart));
LOGGER.at(Level.INFO).log("\u001b[0;32m===============================================================================================");
ServerAuthManager authManager = ServerAuthManager.getInstance();
if (!authManager.isSingleplayer() && authManager.getAuthMode() == ServerAuthManager.AuthMode.NONE) {
LOGGER.at(Level.WARNING).log("%sNo server tokens configured. Use /auth login to authenticate.", "\u001b[0;31m");
}
this.sendSingleplayerSignal(">> Singleplayer Ready <<");
}
}
public void shutdownServer() {
this.shutdownServer(ShutdownReason.SHUTDOWN);
}
public void shutdownServer(@Nonnull ShutdownReason reason) {
Objects.requireNonNull(reason, "Server shutdown reason can't be null!");
if (this.shutdown.getAndSet(reason) == null) {
if (reason.getMessage() != null) {
this.sendSingleplayerSignal("-=|Shutdown|" + reason.getMessage());
}
Thread shutdownThread = new Thread(() -> this.shutdown0(reason), "ShutdownThread");
shutdownThread.setDaemon(false);
shutdownThread.start();
}
}
void shutdown0(@Nonnull ShutdownReason reason) {
LOGGER.at(Level.INFO).log("Shutdown triggered!!!");
try {
LOGGER.at(Level.INFO).log("Shutting down... %d '%s'", reason.getExitCode(), reason.getMessage());
this.eventBus.dispatch(ShutdownEvent.class);
this.pluginManager.shutdown();
this.commandManager.shutdown();
this.eventBus.shutdown();
ServerAuthManager.getInstance().shutdown();
LOGGER.at(Level.INFO).log("Saving config...");
if (this.hytaleServerConfig.consumeHasChanged()) {
HytaleServerConfig.save(this.hytaleServerConfig).join();
}
LOGGER.at(Level.INFO).log("Shutdown completed!");
} catch (Throwable t) {
((HytaleLogger.Api)LOGGER.at(Level.SEVERE).withCause(t)).log("Exception while shutting down:");
}
this.aliveLock.release();
HytaleLogManager.resetFinally();
SCHEDULED_EXECUTOR.schedule(() -> {
LOGGER.at(Level.SEVERE).log("Forcing shutdown!");
Runtime.getRuntime().halt(reason.getExitCode());
}, 3L, TimeUnit.SECONDS);
if (reason != ShutdownReason.SIGINT) {
System.exit(reason.getExitCode());
}
}
public void doneSetup(PluginBase plugin) {
++this.pluginsProgress;
this.sendSingleplayerProgress();
}
public void doneStart(PluginBase plugin) {
++this.pluginsProgress;
this.sendSingleplayerProgress();
}
public void doneStop(PluginBase plugin) {
--this.pluginsProgress;
this.sendSingleplayerProgress();
}
public void sendSingleplayerProgress() {
List<PluginBase> plugins = this.pluginManager.getPlugins();
if (this.shutdown.get() != null) {
double var10001 = MathUtil.round((double)(plugins.size() - this.pluginsProgress) / (double)plugins.size(), 2);
this.sendSingleplayerSignal("-=|Shutdown Modules|" + var10001 * 100.0);
} else if (this.pluginManager.getState() == PluginState.SETUP) {
double var2 = MathUtil.round((double)this.pluginsProgress / (double)plugins.size(), 2);
this.sendSingleplayerSignal("-=|Setup|" + var2 * 100.0);
} else if (this.pluginManager.getState() == PluginState.START) {
double var3 = MathUtil.round((double)this.pluginsProgress / (double)plugins.size(), 2);
this.sendSingleplayerSignal("-=|Starting|" + var3 * 100.0);
}
}
public String getServerName() {
return this.getConfig().getServerName();
}
public boolean isBooting() {
return this.booting.get();
}
public boolean isBooted() {
return this.booted.get();
}
public boolean isShuttingDown() {
return this.shutdown.get() != null;
}
@Nonnull
public Instant getBoot() {
return this.boot;
}
public long getBootStart() {
return this.bootStart;
}
@Nullable
public ShutdownReason getShutdownReason() {
return (ShutdownReason)this.shutdown.get();
}
private void sendSingleplayerSignal(String message) {
if (Constants.SINGLEPLAYER) {
HytaleLoggerBackend.rawLog(message);
}
}
public void reportSingleplayerStatus(String message) {
if (Constants.SINGLEPLAYER) {
HytaleLoggerBackend.rawLog("-=|" + message + "|0");
}
}
public void reportSaveProgress(@Nonnull World world, int saved, int total) {
if (this.isShuttingDown()) {
double progress = MathUtil.round((double)saved / (double)total, 2) * 100.0;
if (Constants.SINGLEPLAYER) {
String var10001 = world.getName();
this.sendSingleplayerSignal("-=|Saving world " + var10001 + " chunks|" + progress);
} else if (total < 10 || saved % (total / 10) == 0) {
world.getLogger().at(Level.INFO).log("Saving chunks: %.0f%%", progress);
}
}
}
public static HytaleServer get() {
return instance;
}
static {
METRICS_REGISTRY = (new MetricsRegistry()).register("Time", (server) -> Instant.now(), Codec.INSTANT).register("Boot", (server) -> server.boot, Codec.INSTANT).register("BootStart", (server) -> server.bootStart, Codec.LONG).register("Booting", (server) -> server.booting.get(), Codec.BOOLEAN).register("ShutdownReason", (server) -> {
ShutdownReason reason = (ShutdownReason)server.shutdown.get();
return reason == null ? null : reason.toString();
}, Codec.STRING).register("PluginManager", HytaleServer::getPluginManager, PluginManager.METRICS_REGISTRY).register("Config", HytaleServer::getConfig, HytaleServerConfig.CODEC).register("JVM", JVMMetrics.METRICS_REGISTRY);
LOGGER = HytaleLogger.forEnclosingClass();
}
}