diff --git a/plugins/MinikuraVelocity/.gitignore b/plugins/MinikuraVelocity/.gitignore
new file mode 100644
index 0000000..5ff6309
--- /dev/null
+++ b/plugins/MinikuraVelocity/.gitignore
@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/plugins/MinikuraVelocity/.run/minikura-velocity.run.xml b/plugins/MinikuraVelocity/.run/minikura-velocity.run.xml
new file mode 100644
index 0000000..20d58b0
--- /dev/null
+++ b/plugins/MinikuraVelocity/.run/minikura-velocity.run.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/MinikuraVelocity/dependency-reduced-pom.xml b/plugins/MinikuraVelocity/dependency-reduced-pom.xml
new file mode 100644
index 0000000..ec214e9
--- /dev/null
+++ b/plugins/MinikuraVelocity/dependency-reduced-pom.xml
@@ -0,0 +1,232 @@
+
+
+ 4.0.0
+ cafe.kirameki
+ minikura-velocity
+ minikura-velocity
+ 1.0
+
+ src/main/kotlin
+ clean package
+
+
+ true
+ ${project.basedir}/src/main/resources
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ kapt
+
+ kapt
+
+
+
+ src/main/kotlin
+ src/main/java
+
+
+
+ com.velocitypowered
+ velocity-api
+ 3.4.0-SNAPSHOT
+
+
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+ 1.8
+
+
+
+ maven-shade-plugin
+ 3.5.3
+
+
+ package
+
+ shade
+
+
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+
+ default-compile
+ none
+
+
+ default-testCompile
+ none
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ testCompile
+ test-compile
+
+ testCompile
+
+
+
+
+ ${java.version}
+ ${java.version}
+
+
+
+ org.codehaus.mojo
+ templating-maven-plugin
+ 1.0.0
+
+
+ filter-src
+
+ filter-sources
+
+
+
+
+
+ maven-site-plugin
+ 3.12.1
+
+
+ net.trajano.wagon
+ wagon-git
+ 2.0.4
+
+
+ org.apache.maven.doxia
+ doxia-module-markdown
+ 1.12.0
+
+
+
+
+ maven-release-plugin
+ 3.0.1
+
+ true
+ @{project.version}
+ [RELEASE]
+ install deploy site-deploy
+ release
+
+
+
+
+
+
+ release
+
+
+
+ true
+ src/main/resources
+
+
+
+
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ maven-javadoc-plugin
+ 3.6.3
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ maven-gpg-plugin
+ 3.2.4
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+
+
+ papermc-repo
+ https://repo.papermc.io/repository/maven-public/
+
+
+
+
+ com.velocitypowered
+ velocity-api
+ 3.4.0-SNAPSHOT
+ provided
+
+
+ org.jetbrains.kotlin
+ kotlin-test
+ 2.1.0
+ test
+
+
+
+
+
+ maven-javadoc-plugin
+ 3.6.3
+
+
+
+
+ 2.1.0
+ 17
+ UTF-8
+
+
diff --git a/plugins/MinikuraVelocity/pom.xml b/plugins/MinikuraVelocity/pom.xml
new file mode 100644
index 0000000..63afc96
--- /dev/null
+++ b/plugins/MinikuraVelocity/pom.xml
@@ -0,0 +1,272 @@
+
+
+ 4.0.0
+
+ cafe.kirameki
+ minikura-velocity
+ 1.0
+ jar
+
+ minikura-velocity
+
+
+ 17
+ 2.1.0
+ UTF-8
+
+
+
+
+ release
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.3
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.4
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+
+
+
+ clean package
+ src/main/kotlin
+
+
+ ${project.basedir}/src/main/resources
+ true
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ kapt
+
+ kapt
+
+
+
+ src/main/kotlin
+ src/main/java
+
+
+
+ com.velocitypowered
+ velocity-api
+ 3.4.0-SNAPSHOT
+
+
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+ 1.8
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.3
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+ default-compile
+ none
+
+
+ default-testCompile
+ none
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ testCompile
+ test-compile
+
+ testCompile
+
+
+
+
+ ${java.version}
+ ${java.version}
+
+
+
+ org.codehaus.mojo
+ templating-maven-plugin
+ 1.0.0
+
+
+ filter-src
+
+ filter-sources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 3.12.1
+
+
+ net.trajano.wagon
+ wagon-git
+ 2.0.4
+
+
+ org.apache.maven.doxia
+ doxia-module-markdown
+ 1.12.0
+
+
+
+
+ org.apache.maven.plugins
+ maven-release-plugin
+ 3.0.1
+
+ true
+ @{project.version}
+ [RELEASE]
+ install deploy site-deploy
+
+ release
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.3
+
+
+
+
+
+
+ papermc-repo
+ https://repo.papermc.io/repository/maven-public/
+
+
+
+
+
+ com.velocitypowered
+ velocity-api
+ 3.4.0-SNAPSHOT
+ provided
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-test
+ ${kotlin.version}
+ test
+
+
+ org.java-websocket
+ Java-WebSocket
+ 1.6.0
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.12.0
+
+
+
diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/Main.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/Main.kt
new file mode 100644
index 0000000..5ecf37e
--- /dev/null
+++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/Main.kt
@@ -0,0 +1,203 @@
+package cafe.kirameki.minikuraVelocity
+
+import cafe.kirameki.minikuraVelocity.listeners.ServerConnectionHandler
+import cafe.kirameki.minikuraVelocity.models.ReverseProxyServerData
+import cafe.kirameki.minikuraVelocity.models.ServerData
+import cafe.kirameki.minikuraVelocity.store.ServerDataStore
+import cafe.kirameki.minikuraVelocity.utils.createWebSocketClient
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.google.inject.Inject
+import com.velocitypowered.api.command.CommandManager
+import com.velocitypowered.api.command.CommandMeta
+import com.velocitypowered.api.command.SimpleCommand
+import com.velocitypowered.api.event.Subscribe
+import com.velocitypowered.api.event.proxy.ProxyInitializeEvent
+import com.velocitypowered.api.plugin.Plugin
+import com.velocitypowered.api.proxy.ProxyServer
+import com.velocitypowered.api.proxy.server.RegisteredServer
+import com.velocitypowered.api.proxy.server.ServerInfo
+import net.kyori.adventure.text.Component
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.slf4j.Logger
+import java.net.InetSocketAddress
+import java.util.concurrent.Executors
+
+@Plugin(id = "minikura-velocity", name = "MinikuraVelocity", version = "1.0")
+class Main @Inject constructor(private val logger: Logger, private val server: ProxyServer) {
+ private val servers: MutableMap = HashMap()
+ private val client = OkHttpClient()
+ private val apiKey: String = System.getenv("MINIKURA_API_KEY") ?: ""
+ private val apiUrl: String = System.getenv("MINIKURA_API_URL") ?: "http://localhost:3000"
+ private val websocketUrl: String = System.getenv("MINIKURA_WEBSOCKET_URL") ?: "ws://localhost:3000/ws"
+
+ @Subscribe
+ fun onProxyInitialization(event: ProxyInitializeEvent?) {
+ logger.info("Minikura-Velocity is initializing...")
+
+ val client = createWebSocketClient(this, server, websocketUrl)
+ client.connect()
+
+ val commandManager: CommandManager = server.commandManager
+
+ val serversCommandMeta: CommandMeta = commandManager.metaBuilder("servers")
+ .plugin(this)
+ .aliases("listservers", "serverlist")
+ .build()
+
+ val serversCommand = SimpleCommand { p ->
+ val source = p.source()
+ source.sendMessage(Component.text("Available servers:"))
+ for ((name, server) in servers) {
+ source.sendMessage(Component.text(" - $name (${server.serverInfo.address})"))
+ }
+ }
+
+ commandManager.register(serversCommandMeta, serversCommand)
+
+ val refreshCommandMeta: CommandMeta = commandManager.metaBuilder("refresh")
+ .plugin(this)
+ .build()
+
+ val refreshCommand = SimpleCommand { p ->
+ val source = p.source()
+ source.sendMessage(Component.text("Refreshing server list..."))
+ fetchServers()
+ fetchReverseProxyServers()
+ source.sendMessage(Component.text("Server list refreshed successfully!"))
+ }
+
+ commandManager.register(refreshCommandMeta, refreshCommand)
+
+ val migrateCommandMeta: CommandMeta = commandManager.metaBuilder("migrate")
+ .plugin(this)
+ .build()
+
+ val migrateCommand = SimpleCommand { p ->
+ val source = p.source()
+ val args = p.arguments()
+
+ if (args.isEmpty()) {
+ source.sendMessage(Component.text("Please specify a server to migrate players to."))
+ return@SimpleCommand
+ }
+
+ val targetServerName = args[0]
+ val targetServer = ServerDataStore.getReverseProxyServer(targetServerName)
+
+ if (targetServer == null) {
+ source.sendMessage(Component.text("Server '$targetServerName' not found."))
+ return@SimpleCommand
+ }
+
+ migratePlayersToServer(targetServer)
+ source.sendMessage(Component.text("Migrating players to server '$targetServerName'..."))
+ }
+
+ commandManager.register(migrateCommandMeta, migrateCommand)
+
+ val connectionHandler = ServerConnectionHandler(servers, logger)
+ server.eventManager.register(this, connectionHandler)
+
+ fetchServers()
+ fetchReverseProxyServers()
+
+ logger.info("Minikura-Velocity has been initialized.")
+ }
+
+ private fun fetchReverseProxyServers() {
+ val request = Request.Builder()
+ .url("$apiUrl/reverse_proxy_servers")
+ .header("Authorization", "Bearer $apiKey")
+ .build()
+
+ Executors.newSingleThreadExecutor().submit {
+ try {
+ val response = client.newCall(request).execute()
+ if (response.isSuccessful) {
+ val responseBody = response.body?.string()
+ val fetchedServers = parseReverseProxyServersData(responseBody)
+
+ server.scheduler.buildTask(this, Runnable {
+ synchronized(ServerDataStore) {
+ ServerDataStore.clearReverseProxyServers()
+ ServerDataStore.addAllReverseProxyServers(fetchedServers)
+ }
+ }).schedule()
+ } else {
+ logger.error("Failed to fetch reverse proxy servers: ${response.message}")
+ }
+ } catch (e: Exception) {
+ logger.error("Error fetching reverse proxy servers: ${e.message}", e)
+ }
+ }
+ }
+
+ private fun fetchServers() {
+ server.allServers.forEach { server.unregisterServer(it.serverInfo) }
+ val request = Request.Builder()
+ .url("$apiUrl/servers")
+ .header("Authorization", "Bearer $apiKey")
+ .build()
+
+ Executors.newSingleThreadExecutor().submit {
+ try {
+ val response = client.newCall(request).execute()
+ if (response.isSuccessful) {
+ val responseBody = response.body?.string()
+ val fetchedServers = parseServersData(responseBody)
+
+ server.scheduler.buildTask(this, Runnable {
+ synchronized(ServerDataStore) {
+ ServerDataStore.clearServers()
+ ServerDataStore.addAllServers(fetchedServers)
+ }
+ populateServers(fetchedServers)
+ }).schedule()
+ } else {
+ logger.error("Failed to fetch servers: ${response.message}")
+ }
+ } catch (e: Exception) {
+ logger.error("Error fetching servers: ${e.message}", e)
+ }
+ }
+ }
+
+ private fun parseReverseProxyServersData(responseBody: String?): List {
+ if (responseBody.isNullOrEmpty()) return emptyList()
+
+ val gson = Gson()
+ val reverseProxyServerListType = object : TypeToken>() {}.type
+ return gson.fromJson(responseBody, reverseProxyServerListType)
+ }
+
+ private fun parseServersData(responseBody: String?): List {
+ if (responseBody.isNullOrEmpty()) return emptyList()
+
+ val gson = Gson()
+
+ val serverListType = object : TypeToken>() {}.type
+ return gson.fromJson(responseBody, serverListType)
+ }
+
+ private fun populateServers(serversData: List) {
+ servers.clear()
+
+ for (data in serversData) {
+ val serverInfo = ServerInfo(data.name, InetSocketAddress(data.address, data.port))
+ val registeredServer = server.createRawRegisteredServer(serverInfo)
+ servers[data.name] = registeredServer
+ this.server.registerServer(registeredServer.serverInfo)
+ }
+ }
+
+ private fun migratePlayersToServer(targetServer: ReverseProxyServerData) {
+ val targetAddress = InetSocketAddress(targetServer.address, targetServer.port)
+
+ server.allPlayers.forEach { player ->
+ player.transferToHost(targetAddress)
+ }
+ }
+
+}
diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/MinikuraWebSocketClient.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/MinikuraWebSocketClient.kt
new file mode 100644
index 0000000..9cb3683
--- /dev/null
+++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/MinikuraWebSocketClient.kt
@@ -0,0 +1,26 @@
+package cafe.kirameki.minikuraVelocity
+
+import com.velocitypowered.api.proxy.ProxyServer
+import org.java_websocket.client.WebSocketClient
+import org.java_websocket.handshake.ServerHandshake
+import java.net.URI
+import java.time.Duration
+
+class MinikuraWebSocketClient(private val plugin: Main, private val server: ProxyServer, serverUri: URI?) : WebSocketClient(serverUri) {
+ override fun onOpen(handshakedata: ServerHandshake) {
+ println("Connected to server")
+ }
+
+ override fun onMessage(message: String) {
+ println("Received: $message")
+ }
+
+ override fun onError(ex: Exception) {
+ ex.printStackTrace()
+ }
+
+ override fun onClose(code: Int, reason: String, remote: Boolean) {
+ println("Disconnected from websocket, reconnecting...")
+ server.scheduler.buildTask(plugin, Runnable { reconnect() }).delay(Duration.ofMillis(5000)).schedule()
+ }
+}
\ No newline at end of file
diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/datastore/ServerDataStore.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/datastore/ServerDataStore.kt
new file mode 100644
index 0000000..e278d05
--- /dev/null
+++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/datastore/ServerDataStore.kt
@@ -0,0 +1,54 @@
+package cafe.kirameki.minikuraVelocity.store
+
+import cafe.kirameki.minikuraVelocity.models.ReverseProxyServerData
+import cafe.kirameki.minikuraVelocity.models.ServerData
+import com.velocitypowered.api.proxy.ProxyServer
+
+object ServerDataStore {
+ private val servers: MutableList = mutableListOf()
+ private val reverseProxyServers: MutableList = mutableListOf()
+
+ fun initialize(server: ProxyServer) {
+
+ }
+
+ fun addServer(serverData: ServerData) {
+ servers.add(serverData)
+ }
+
+ fun addReverseProxyServer(reverseProxyServerData: ReverseProxyServerData) {
+ reverseProxyServers.add(reverseProxyServerData)
+ }
+
+ fun addAllServers(serverData: List) {
+ servers.addAll(serverData)
+ }
+
+ fun addAllReverseProxyServers(reverseProxyServerData: List) {
+ reverseProxyServers.addAll(reverseProxyServerData)
+ }
+
+ fun getServer(name: String): ServerData? {
+ return servers.find { it.name == name }
+ }
+
+ fun getReverseProxyServer(name: String): ReverseProxyServerData? {
+ return reverseProxyServers.find { it.name == name }
+ }
+
+ fun getServers(): List {
+ return servers
+ }
+
+ fun getReverseProxyServers(): List {
+ return reverseProxyServers
+ }
+
+ fun clearServers() {
+ servers.clear()
+ }
+
+ fun clearReverseProxyServers() {
+ reverseProxyServers.clear()
+ }
+}
diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/listeners/ServerConnectionHandler.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/listeners/ServerConnectionHandler.kt
new file mode 100644
index 0000000..2ae9505
--- /dev/null
+++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/listeners/ServerConnectionHandler.kt
@@ -0,0 +1,35 @@
+package cafe.kirameki.minikuraVelocity.listeners
+
+import cafe.kirameki.minikuraVelocity.store.ServerDataStore
+import com.velocitypowered.api.event.Subscribe
+import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent
+import com.velocitypowered.api.proxy.server.RegisteredServer
+import org.slf4j.Logger
+
+class ServerConnectionHandler(
+ private val servers: Map,
+ private val logger: Logger
+) {
+ @Subscribe
+ fun onPlayerChooseInitialServer(event: PlayerChooseInitialServerEvent) {
+ val player = event.player
+ val currentServer = event.initialServer;
+
+ val sortedServers = ServerDataStore.getServers()
+ .filter { it.join_priority != null }
+ .sortedBy { it.join_priority }
+ .mapNotNull { servers[it.name] }
+
+ System.out.println("Sorted servers: ${sortedServers.map { it.serverInfo.name }}")
+
+ for (server in sortedServers) {
+ if (server != currentServer) {
+ logger.info("Attempting to connect ${player.username} to server ${server.serverInfo.name}")
+ event.setInitialServer(server)
+ return
+ }
+ }
+
+ logger.warn("No available servers with valid join_priority for ${player.username}")
+ }
+}
\ No newline at end of file
diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ReverseProxyServerData.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ReverseProxyServerData.kt
new file mode 100644
index 0000000..f3fb034
--- /dev/null
+++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ReverseProxyServerData.kt
@@ -0,0 +1,12 @@
+package cafe.kirameki.minikuraVelocity.models
+
+data class ReverseProxyServerData(
+ val id: String,
+ val name: String,
+ val description: String?,
+ val address: String,
+ val port: Int,
+ val api_key: String,
+ val created_at: String,
+ val updated_at: String
+)
\ No newline at end of file
diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ServerData.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ServerData.kt
new file mode 100644
index 0000000..f0c5481
--- /dev/null
+++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ServerData.kt
@@ -0,0 +1,14 @@
+package cafe.kirameki.minikuraVelocity.models
+
+data class ServerData(
+ val id: String,
+ val name: String,
+ val description: String?,
+ val address: String,
+ val port: Int,
+ val type: String,
+ val join_priority: Int?,
+ val api_key: String,
+ val created_at: String,
+ val updated_at: String
+)
\ No newline at end of file
diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/utils/WebSocketUtils.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/utils/WebSocketUtils.kt
new file mode 100644
index 0000000..a2e95c6
--- /dev/null
+++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/utils/WebSocketUtils.kt
@@ -0,0 +1,12 @@
+package cafe.kirameki.minikuraVelocity.utils
+
+import cafe.kirameki.minikuraVelocity.Main
+import cafe.kirameki.minikuraVelocity.MinikuraWebSocketClient
+import com.velocitypowered.api.proxy.ProxyServer
+import java.net.URI
+
+fun createWebSocketClient(plugin: Main, server: ProxyServer, websocketUrl: String): MinikuraWebSocketClient {
+ val uri = URI(websocketUrl)
+ val client = MinikuraWebSocketClient(plugin, server, uri)
+ return client
+}