Add VelocityCore Module

This commit is contained in:
2024-08-04 21:05:46 +02:00
parent 3e6f8c9b91
commit 28d4886584
145 changed files with 15181 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
plugins {
id("java")
}
group = "de.steamwar"
tasks.compileJava {
options.encoding = "UTF-8"
}
sourceSets {
main {
java {
srcDirs("src/")
}
resources {
srcDirs("src/")
exclude("**/*.java", "**/*.kt")
}
}
}
dependencies {
compileOnly("org.projectlombok:lombok:1.18.32")
annotationProcessor("org.projectlombok:lombok:1.18.32")
compileOnly("de.steamwar:velocity:RELEASE")
annotationProcessor("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT")
}
@@ -0,0 +1,37 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.persistent;
import lombok.Getter;
@Getter
public class Arenaserver extends Subserver {
private final String mode;
private final String map;
private final boolean allowMerge;
public Arenaserver(String serverName, String mode, String map, boolean allowMerge, int port, ProcessBuilder processBuilder, Runnable shutdownCallback) {
super(Servertype.ARENA, serverName, port, processBuilder, shutdownCallback, null);
this.mode = mode;
this.map = map;
this.allowMerge = allowMerge;
}
}
@@ -0,0 +1,60 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.persistent;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
@Getter
public class Bauserver extends Subserver {
private static final Map<UUID, Bauserver> servers = new HashMap<>();
public static Bauserver get(UUID owner) {
synchronized (servers) {
return servers.get(owner);
}
}
private final UUID owner;
public Bauserver(String serverName, UUID owner, int port, ProcessBuilder processBuilder, Runnable shutdownCallback){
this(serverName, owner, port, processBuilder, shutdownCallback, null);
}
public Bauserver(String serverName, UUID owner, int port, ProcessBuilder processBuilder, Runnable shutdownCallback, Consumer<Exception> failureCallback){
super(Servertype.BAUSERVER, serverName, port, processBuilder, shutdownCallback, failureCallback);
this.owner = owner;
synchronized (servers) {
servers.put(owner, this);
}
}
@Override
protected void unregister() {
synchronized (servers) {
servers.remove(owner);
}
super.unregister();
}
}
@@ -0,0 +1,59 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.persistent;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
@Getter
public class Builderserver extends Subserver {
private static final Map<String, Builderserver> servers = new HashMap<>();
public static Builderserver get(String map) {
synchronized (servers) {
return servers.get(map);
}
}
private final String map;
public Builderserver(String serverName, String map, int port, ProcessBuilder processBuilder, Runnable shutdownCallback){
this(serverName, map, port, processBuilder, shutdownCallback, null);
}
public Builderserver(String serverName, String map, int port, ProcessBuilder processBuilder, Runnable shutdownCallback, Consumer<Exception> failureCallback){
super(Servertype.BUILDER, serverName, port, processBuilder, shutdownCallback, failureCallback);
this.map = map;
synchronized (servers) {
servers.put(map, this);
}
}
@Override
protected void unregister() {
synchronized (servers) {
servers.remove(map);
}
super.unregister();
}
}
@@ -0,0 +1,203 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.persistent;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.name.Names;
import com.mojang.brigadier.Command;
import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.command.CommandMeta;
import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.scheduler.ScheduledTask;
import com.velocitypowered.proxy.plugin.PluginClassLoader;
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer;
import com.velocitypowered.proxy.plugin.loader.java.JavaPluginLoader;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import java.io.IOException;
import java.nio.file.Path;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@Plugin(
id = "persistentvelocitycore",
name = "PersistentVelocityCore"
)
public class Persistent {
private static final Reflection.Method<VelocityPluginManager> registerPlugin = new Reflection.Method<>(VelocityPluginManager.class, "registerPlugin", PluginContainer.class);
@Getter
private static Persistent instance;
@Getter
private final ProxyServer proxy;
@Getter
private final Logger logger;
private final Path directory;
@Inject
public Persistent(ProxyServer proxy, Logger logger, @DataDirectory Path dataDirectory) {
instance = this;
this.proxy = proxy;
this.logger = logger;
this.directory = dataDirectory;
}
@Subscribe
public void onEnable(ProxyInitializeEvent event) {
proxy.getCommandManager().register(
new BrigadierCommand(
BrigadierCommand.literalArgumentBuilder("softreload")
.requires(commandSource -> commandSource.hasPermission("bungeecore.softreload"))
.executes(commandContext -> softreload())
.build()
)
);
}
@Subscribe
public void onDisable(ProxyShutdownEvent event) {
Subserver.shutdown();
}
public int softreload() {
PluginContainer container = null;
ReloadablePlugin plugin = null;
try {
container = proxy.getPluginManager().getPlugin("velocitycore").orElseThrow();
plugin = (ReloadablePlugin) container.getInstance().orElseThrow();
} catch (NoSuchElementException e) {
logger.log(Level.WARNING, "Could not find loaded VelocityCore, continuing without unloading.");
}
PluginContainer newContainer;
try {
newContainer = prepareLoad();
} catch (Exception e) {
logger.log(Level.SEVERE, "Could not instantiate new VelocityCore, aborting softreload.", e);
return Command.SINGLE_SUCCESS;
}
broadcast("§eNetwork update is starting§8.");
try {
if(container != null && plugin != null) {
plugin.onProxyShutdown(new ProxyShutdownEvent());
unload(container, plugin);
}
registerPlugin.invoke((VelocityPluginManager) proxy.getPluginManager(), newContainer);
((ReloadablePlugin) newContainer.getInstance().orElseThrow()).onProxyInitialization(new ProxyInitializeEvent());
} catch (Throwable t) {
logger.log(Level.SEVERE, "Error during softreload", t);
broadcast("§cNetwork update failed§8, §cexpect network restart soon§8.");
return Command.SINGLE_SUCCESS;
}
broadcast("§eNetwork update complete§8.");
return Command.SINGLE_SUCCESS;
}
private void broadcast(String message) {
Component component = LegacyComponentSerializer.legacySection().deserialize("§eSteam§8War» " + message);
proxy.getAllPlayers().forEach(player -> player.sendMessage(component));
proxy.getConsoleCommandSource().sendMessage(component);
}
private PluginContainer prepareLoad() throws Exception {
Path plugins = directory.getParent();
JavaPluginLoader loader = new JavaPluginLoader(proxy, plugins);
PluginDescription description = loader.createPluginFromCandidate(loader.loadCandidate(plugins.resolve("VelocityCore.jar")));
PluginContainer container = new VelocityPluginContainer(description);
AbstractModule commonModule = new AbstractModule() {
@Override
protected void configure() {
this.bind(ProxyServer.class).toInstance(proxy);
this.bind(PluginManager.class).toInstance(proxy.getPluginManager());
this.bind(EventManager.class).toInstance(proxy.getEventManager());
this.bind(CommandManager.class).toInstance(proxy.getCommandManager());
this.bind(PluginContainer.class).annotatedWith(Names.named(container.getDescription().getId())).toInstance(container);
}
};
Module module = loader.createModule(container);
loader.createPlugin(container, module, commonModule);
return container;
}
private void unload(PluginContainer container, Object plugin) throws InterruptedException, IOException {
PluginClassLoader classLoader = ((PluginClassLoader) plugin.getClass().getClassLoader());
CommandManager commandManager = proxy.getCommandManager();
for(String alias : commandManager.getAliases()) {
CommandMeta meta = commandManager.getCommandMeta(alias);
if(meta != null && meta.getPlugin() == plugin)
commandManager.unregister(meta);
}
proxy.getEventManager().unregisterListeners(plugin);
proxy.getScheduler().tasksByPlugin(plugin).forEach(ScheduledTask::cancel);
container.getExecutorService().shutdown();
if(!container.getExecutorService().awaitTermination(100, TimeUnit.MILLISECONDS))
logger.log(Level.WARNING, "ExecutorService termination took longer than 100ms, continuing.");
for(Thread thread : Thread.getAllStackTraces().keySet()) {
if(thread.getClass().getClassLoader() != classLoader)
continue;
thread.interrupt();
thread.join(100);
if (thread.isAlive())
logger.log(Level.WARNING, "Could not stop thread %s of plugin %s. Still running".formatted(thread.getName(), container.getDescription().getId()));
}
//TODO close all log handlers
/*for (Handler handler : plugin.getLogger().getHandlers()) {
handler.close();
}*/
//Clear resource bundle cache
ResourceBundle.clearCache(classLoader);
classLoader.close();
}
}
@@ -0,0 +1,75 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.persistent;
import lombok.experimental.UtilityClass;
@UtilityClass
public class Reflection {
public static class Field<C, T> {
private final java.lang.reflect.Field f;
public Field(Class<C> target, String name) {
try {
f = target.getDeclaredField(name);
f.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Cannot find field with name " + name, e);
}
}
public T get(C target) {
try {
return (T) f.get(target);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Cannot access reflection.", e);
}
}
public void set(C target, T value) {
try {
f.set(target, value);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Cannot access reflection.", e);
}
}
}
public static class Method<C> {
private final java.lang.reflect.Method m;
public Method(Class<C> clazz, String methodName, Class<?>... params) {
try {
m = clazz.getDeclaredMethod(methodName, params);
m.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Cannot find method with name " + methodName, e);
}
}
public Object invoke(C target, Object... arguments) {
try {
return m.invoke(target, arguments);
} catch (Exception e) {
throw new IllegalArgumentException("Cannot invoke method " + m, e);
}
}
}
}
@@ -0,0 +1,28 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.persistent;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
public interface ReloadablePlugin {
void onProxyInitialization(ProxyInitializeEvent event);
default void onProxyShutdown(ProxyShutdownEvent event) {}
}
@@ -0,0 +1,26 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.persistent;
public enum Servertype {
BAUSERVER,
ARENA,
BUILDER
}
@@ -0,0 +1,52 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.persistent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import lombok.experimental.UtilityClass;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@UtilityClass
public class Storage {
public static final Map<Player, List<Player>> challenges = new HashMap<>();
public static final Map<Player, Player> lastChats = new HashMap<>();
public static final Map<Integer, List<Integer>> teamInvitations = new HashMap<>(); // UserID -> List<TeamIDs>
public static final Map<Player, Timestamp> sessions = new HashMap<>(); // Contains session start timestamp
public static final Map<Integer, Subserver> eventServer = new HashMap<>(); // TeamID -> Subserver map
public static final Map<Player, Integer> fabricCheckedPlayers = new HashMap<>();
public static final Map<Player, Long> fabricExpectPluginMessage = new HashMap<>();
public static final Map<Integer, ServerInfo> teamServers = new HashMap<>(); // TeamID -> ServerInfo map
public static final Map<Player, Map<UUID, UpsertPlayerInfoPacket.Entry>> directTabItems = new HashMap<>();
}
@@ -0,0 +1,263 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.persistent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import java.io.*;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
@SuppressWarnings("unused")
public class Subserver {
private static final Component PREFIX = Component
.text("Steam").color(NamedTextColor.YELLOW)
.append(Component.text("War» ").color(NamedTextColor.DARK_GRAY));
private static final Logger logger = Persistent.getInstance().getLogger();
@Getter
private static final List<Subserver> serverList = new LinkedList<>();
private static final Map<ServerInfo, Subserver> infoToServer = new HashMap<>();
public static Subserver getSubserver(Player p) {
synchronized (serverList) {
for (int i = serverList.size() - 1; i >= 0; i--) {
if (serverList.get(i).onServer(p))
return serverList.get(i);
}
}
return null;
}
public static Subserver getSubserver(ServerInfo server) {
synchronized (serverList) {
return infoToServer.get(server);
}
}
static void shutdown() {
while (!serverList.isEmpty()) {
Subserver server = serverList.get(0);
server.stop();
}
}
private final String serverName;
private final boolean checkpoint;
private final Runnable shutdownCallback;
private final Consumer<Exception> failureCallback;
private final Process process;
private final PrintWriter writer;
@Getter
private final ServerInfo server;
@Getter
private RegisteredServer registeredServer;
@Getter
private final Servertype type;
private final Thread thread;
@Getter
private boolean started;
private final List<Player> cachedPlayers = new LinkedList<>();
@Getter
private final Map<Player, String> tablistNames = new HashMap<>();
protected Subserver(Servertype type, String serverName, int port, ProcessBuilder processBuilder, Runnable shutdownCallback, Consumer<Exception> failureCallback) {
this.started = false;
this.serverName = serverName;
this.type = type;
this.shutdownCallback = shutdownCallback;
this.failureCallback = failureCallback == null ? this::fatalError : failureCallback;
this.checkpoint = processBuilder.command().contains("criu");
try {
this.process = processBuilder.start();
} catch (IOException e) {
throw new SecurityException("Server could not be started", e);
}
InetSocketAddress address = new InetSocketAddress("127.0.0.1", port);
this.server = new ServerInfo(serverName, address);
this.writer = new PrintWriter(process.getOutputStream(), true);
this.thread = new Thread(this::run, "Subserver " + serverName);
this.thread.start();
}
public void sendPlayer(Player p) {
if (!started) {
p.sendActionBar(generateBar(0));
cachedPlayers.add(p);
} else {
p.createConnectionRequest(registeredServer).connect();
}
}
public void execute(String command) {
writer.println(command);
}
public void stop() {
try {
long pid = process.pid();
if (checkpoint)
pid = process.children().findAny().map(ProcessHandle::pid).orElse(pid);
Runtime.getRuntime().exec(new String[]{"kill", "-SIGUSR1", Long.toString(pid)});
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to send SIGUSR1 to subserver.", e);
}
try {
if (!process.waitFor(1, TimeUnit.MINUTES)) {
logger.log(Level.SEVERE, () -> serverName + " did not stop correctly, forcibly stopping!");
process.destroyForcibly();
}
thread.join();
} catch (InterruptedException e) {
logger.log(Level.SEVERE, "Subserver stop interrupted!", e);
Thread.currentThread().interrupt();
}
}
private boolean onServer(Player p) {
return cachedPlayers.contains(p) || (registeredServer != null && registeredServer.getPlayersConnected().contains(p));
}
private void fatalError(Exception e) {
logger.log(Level.SEVERE, e, () -> serverName + " did not run correctly!");
for (Player cached : cachedPlayers)
cached.sendMessage(PREFIX.append(Component.text("Unexpected error during server startup.").color(NamedTextColor.RED)));
if (registeredServer != null) {
for (Player player : registeredServer.getPlayersConnected())
player.sendMessage(PREFIX.append(Component.text("Lost connection to server.").color(NamedTextColor.RED)));
}
}
private void start(InputStream stream, Predicate<String> test) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
String line = "";
while (!started && (line = reader.readLine()) != null) {
started = test.test(line);
}
if (line == null)
throw new IOException(serverName + " did not start correctly!");
}
}
protected void register() {
if (Persistent.getInstance().getProxy().getServer(serverName).isPresent()) {
SecurityException e = new SecurityException("Server already registered: " + serverName);
stop();
failureCallback.accept(e);
throw e;
}
synchronized (serverList) {
registeredServer = Persistent.getInstance().getProxy().registerServer(server);
serverList.add(this);
infoToServer.put(server, this);
}
}
protected void unregister() {
synchronized (serverList) {
infoToServer.remove(server);
serverList.remove(this);
Persistent.getInstance().getProxy().unregisterServer(server);
registeredServer = null;
}
}
private void run() {
register();
Exception ex = null;
try {
if (checkpoint) {
start(process.getErrorStream(), line -> line.contains("Restore finished successfully."));
} else {
start(process.getInputStream(), line -> {
if (line.contains("Loading libraries, please wait"))
sendProgress(2);
else if (line.contains("Starting Minecraft server on"))
sendProgress(4);
else if (line.contains("Preparing start region"))
sendProgress(6);
return line.contains("Finished mapping loading");
});
}
if (!started)
return;
sendProgress(8);
Thread.sleep(300);
sendProgress(10);
for (Player cachedPlayer : cachedPlayers) {
sendPlayer(cachedPlayer);
}
cachedPlayers.clear();
process.waitFor();
} catch (IOException e) {
ex = e;
} catch (InterruptedException e) {
ex = e;
Thread.currentThread().interrupt();
} finally {
unregister();
shutdownCallback.run();
if (ex != null)
failureCallback.accept(ex);
}
}
private Component generateBar(int progress) {
return Component.text("".repeat(Math.max(0, progress))).color(NamedTextColor.YELLOW)
.append(Component.text("".repeat(Math.max(0, 10 - progress))).color(NamedTextColor.DARK_GRAY));
}
private void sendProgress(int progress) {
Component tc = generateBar(progress);
for (Player cached : cachedPlayers)
cached.sendActionBar(tc);
}
}