From f60312a5c96c96c3521009c7f7680f26ba08c7b6 Mon Sep 17 00:00:00 2001 From: okx-code Date: Wed, 13 May 2026 05:12:39 +0100 Subject: [PATCH] phase 5 --- ansible/build.gradle.kts | 1 + .../net/civmc/zorweth/FlightComputer.java | 2 +- plugins/zorweth-velocity/build.gradle.kts | 14 ++ .../velocity/ZorwethVelocityPlugin.java | 215 ++++++++++++++++++ .../src/main/resources/config.yml | 14 ++ settings.gradle.kts | 1 + 6 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 plugins/zorweth-velocity/build.gradle.kts create mode 100644 plugins/zorweth-velocity/src/main/java/net/civmc/zorweth/velocity/ZorwethVelocityPlugin.java create mode 100644 plugins/zorweth-velocity/src/main/resources/config.yml diff --git a/ansible/build.gradle.kts b/ansible/build.gradle.kts index 1a2dd15c2..73bd85cf4 100644 --- a/ansible/build.gradle.kts +++ b/ansible/build.gradle.kts @@ -102,6 +102,7 @@ dependencies { proxyPlugin(project(path = ":plugins:announcements-velocity", configuration = "shadow")) proxyPlugin(project(path = ":plugins:kiragateway-velocity", configuration = "shadow")) proxyPlugin(project(path = ":plugins:namelayer-velocity", configuration = "shadow")) + proxyPlugin(project(path = ":plugins:zorweth-velocity", configuration = "shadow")) } val copyPaperPlugins = tasks.register("copyPaperPlugins") { diff --git a/plugins/zorweth-paper/src/main/java/net/civmc/zorweth/FlightComputer.java b/plugins/zorweth-paper/src/main/java/net/civmc/zorweth/FlightComputer.java index 0e926eb29..f5d798dc3 100644 --- a/plugins/zorweth-paper/src/main/java/net/civmc/zorweth/FlightComputer.java +++ b/plugins/zorweth-paper/src/main/java/net/civmc/zorweth/FlightComputer.java @@ -584,7 +584,7 @@ public final class FlightComputer implements Listener { for (final RocketManifestPassenger passenger : manifest.passengers()) { if (Bukkit.getPlayer(passenger.playerUuid()) == null) { markPreparedTransferCancelled(manifest); - clicker.sendMessage(Component.text("Passenger disconnected before launch commit completed.", NamedTextColor.RED)); + clicker.sendMessage(Component.text("Passenger disconnected before launch completed.", NamedTextColor.RED)); return; } } diff --git a/plugins/zorweth-velocity/build.gradle.kts b/plugins/zorweth-velocity/build.gradle.kts new file mode 100644 index 000000000..987a2e8d9 --- /dev/null +++ b/plugins/zorweth-velocity/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.shadow) +} + +version = "1.0.0" + +dependencies { + compileOnly(libs.velocity.api) + annotationProcessor(libs.velocity.api) + + api(libs.configurate.yaml) + api(libs.hikaricp) + api(libs.mariadb.client) +} diff --git a/plugins/zorweth-velocity/src/main/java/net/civmc/zorweth/velocity/ZorwethVelocityPlugin.java b/plugins/zorweth-velocity/src/main/java/net/civmc/zorweth/velocity/ZorwethVelocityPlugin.java new file mode 100644 index 000000000..20a21d966 --- /dev/null +++ b/plugins/zorweth-velocity/src/main/java/net/civmc/zorweth/velocity/ZorwethVelocityPlugin.java @@ -0,0 +1,215 @@ +package net.civmc.zorweth.velocity; + +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.event.player.ServerPreConnectEvent; +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.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.slf4j.Logger; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +@Plugin(id = "zorweth", name = "Zorweth", version = "1.0.0", authors = {"Okx"}) +public final class ZorwethVelocityPlugin { + + private static final String TRANSFER_SOURCE_CLEARED = "SOURCE_CLEARED"; + private static final String TRANSFER_CLAIMED = "CLAIMED"; + private static final String TRANSFER_PREPARED = "PREPARED"; + + private final ProxyServer server; + private final Logger logger; + private final Path dataDirectory; + private CommentedConfigurationNode config; + private HikariDataSource dataSource; + private RocketTransferRouter router; + + @Inject + public ZorwethVelocityPlugin(final ProxyServer server, final Logger logger, + @DataDirectory final Path dataDirectory) { + this.server = server; + this.logger = logger; + this.dataDirectory = dataDirectory; + } + + @Subscribe + public void onProxyInitialization(final ProxyInitializeEvent event) { + loadConfig(); + this.dataSource = createDataSource(); + this.router = new RocketTransferRouter(this.dataSource); + } + + @Subscribe + public void onProxyShutdown(final ProxyShutdownEvent event) { + if (this.dataSource != null) { + this.dataSource.close(); + this.dataSource = null; + } + } + + @Subscribe(priority = -1) + public void onServerPreConnect(final ServerPreConnectEvent event) { + final PendingTransfer transfer; + try { + transfer = this.router.getPendingTransfer(event.getPlayer().getUniqueId()); + } catch (final SQLException exception) { + this.logger.error("Failed to look up pending rocket transfer", exception); + routeToHoldingOrDisconnect(event); + return; + } + + if (transfer == null || TRANSFER_PREPARED.equals(transfer.state())) { + return; + } + if (!TRANSFER_SOURCE_CLEARED.equals(transfer.state()) && !TRANSFER_CLAIMED.equals(transfer.state())) { + return; + } + + final Optional destination = this.server.getServer(transfer.destinationServer()); + if (destination.isEmpty()) { + routeToHoldingOrDisconnect(event); + return; + } + event.setResult(ServerPreConnectEvent.ServerResult.allowed(destination.get())); + } + + @Subscribe + public void onServerPostConnect(final ServerPostConnectEvent event) { + final Optional currentServer = event.getPlayer().getCurrentServer(); + if (currentServer.isEmpty()) { + return; + } + + try { + this.router.updateLastServer( + event.getPlayer().getUniqueId(), + currentServer.get().getServerInfo().getName() + ); + } catch (final SQLException exception) { + this.logger.error("Failed to update player last server", exception); + } + } + + private void routeToHoldingOrDisconnect(final ServerPreConnectEvent event) { + final String holdingServer = this.config.node("holding-server").getString("").trim(); + if (!holdingServer.isEmpty()) { + final Optional server = this.server.getServer(holdingServer); + if (server.isPresent()) { + event.setResult(ServerPreConnectEvent.ServerResult.allowed(server.get())); + return; + } + } + event.getPlayer().disconnect(Component.text( + this.config.node("failure-message").getString("Unable to verify your rocket transfer. Please reconnect and try again."), + NamedTextColor.RED + )); + } + + private void loadConfig() { + try { + if (!Files.exists(this.dataDirectory)) { + Files.createDirectories(this.dataDirectory); + } + final Path configFile = this.dataDirectory.resolve("config.yml"); + if (!Files.exists(configFile)) { + try (InputStream input = getClass().getResourceAsStream("/config.yml")) { + if (input == null) { + throw new IllegalStateException("Default config.yml is missing"); + } + Files.copy(input, configFile); + } + } + this.config = YamlConfigurationLoader.builder().path(configFile).build().load(); + } catch (final IOException exception) { + throw new RuntimeException("Could not load Zorweth Velocity config", exception); + } + } + + private HikariDataSource createDataSource() { + final CommentedConfigurationNode database = this.config.node("database"); + final HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl("jdbc:" + database.node("driver").getString("mariadb") + "://" + + database.node("host").getString("localhost") + ":" + + database.node("port").getInt(3306) + "/" + + database.node("database").getString("database")); + hikariConfig.setConnectionTimeout(database.node("connection_timeout").getLong(10_000L)); + hikariConfig.setIdleTimeout(database.node("idle_timeout").getLong(600_000L)); + hikariConfig.setMaxLifetime(database.node("max_lifetime").getLong(7_200_000L)); + hikariConfig.setMaximumPoolSize(database.node("poolsize").getInt(5)); + hikariConfig.setUsername(database.node("user").getString("root")); + final String password = database.node("password").getString(""); + if (!password.isBlank()) { + hikariConfig.setPassword(password); + } + return new HikariDataSource(hikariConfig); + } + + private record PendingTransfer(String state, String destinationServer) { + } + + private static final class RocketTransferRouter { + + private final HikariDataSource dataSource; + + private RocketTransferRouter(final HikariDataSource dataSource) { + this.dataSource = dataSource; + } + + private PendingTransfer getPendingTransfer(final UUID playerUuid) throws SQLException { + try (Connection connection = this.dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(""" + SELECT rt.state, rt.destination_server + FROM rocket_transfer_players rtp + JOIN rocket_transfers rt ON rt.transfer_id = rtp.transfer_id + WHERE rtp.player_uuid = ? + AND rt.state IN ('PREPARED', 'SOURCE_CLEARED', 'CLAIMED') + AND rtp.state IN ('PENDING', 'CLAIMED') + ORDER BY rt.created_at DESC + LIMIT 1 + """)) { + statement.setString(1, playerUuid.toString()); + try (ResultSet resultSet = statement.executeQuery()) { + if (!resultSet.next()) { + return null; + } + return new PendingTransfer( + resultSet.getString("state"), + resultSet.getString("destination_server") + ); + } + } + } + + private void updateLastServer(final UUID playerUuid, final String serverName) throws SQLException { + try (Connection connection = this.dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(""" + INSERT INTO player_server_state (player_uuid, last_server) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE last_server = VALUES(last_server) + """)) { + statement.setString(1, playerUuid.toString()); + statement.setString(2, serverName); + statement.executeUpdate(); + } + } + } +} diff --git a/plugins/zorweth-velocity/src/main/resources/config.yml b/plugins/zorweth-velocity/src/main/resources/config.yml new file mode 100644 index 000000000..781dce759 --- /dev/null +++ b/plugins/zorweth-velocity/src/main/resources/config.yml @@ -0,0 +1,14 @@ +database: + driver: mariadb + user: root + password: root + host: localhost + port: 3306 + database: database + poolsize: 5 + connection_timeout: 10000 + idle_timeout: 600000 + max_lifetime: 7200000 + +holding-server: "" +failure-message: "Unable to verify your rocket transfer. Please reconnect and try again." diff --git a/settings.gradle.kts b/settings.gradle.kts index c81724173..ec98a67d4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,6 +47,7 @@ include(":plugins:kitpvp-paper") include(":plugins:voidworld-paper") include(":plugins:heliodor-paper") include(":plugins:zorweth-paper") +include(":plugins:zorweth-velocity") include(":plugins:civproxy-velocity") include(":plugins:secureboot-paper") include(":plugins:kiragateway-velocity")