├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question.yml │ ├── feature_request.yml │ └── bug_report.yml └── workflows │ └── Build.yml ├── settings.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── renovate.json ├── src └── main │ ├── kotlin │ └── work │ │ └── msdnicrosoft │ │ └── avm │ │ ├── annotations │ │ ├── dsl │ │ │ ├── ComponentDSL.kt │ │ │ └── CommandDSL.kt │ │ └── command │ │ │ ├── RootCommand.kt │ │ │ └── CommandNode.kt │ │ ├── util │ │ ├── command │ │ │ ├── data │ │ │ │ ├── server │ │ │ │ │ ├── Server.kt │ │ │ │ │ └── ServerGroup.kt │ │ │ │ ├── PlayerByUUID.kt │ │ │ │ └── component │ │ │ │ │ └── MiniMessage.kt │ │ │ ├── builder │ │ │ │ ├── Command.kt │ │ │ │ ├── LiteralCommand.kt │ │ │ │ ├── ArgumentCommand.kt │ │ │ │ └── CommandDSL.kt │ │ │ ├── CommandExtension.kt │ │ │ └── context │ │ │ │ ├── CommandSourceExtension.kt │ │ │ │ ├── CommandContext.kt │ │ │ │ ├── CommandContextExtension.kt │ │ │ │ └── ArgumentParser.kt │ │ ├── packet │ │ │ ├── data │ │ │ │ ├── codec │ │ │ │ │ ├── Encoder.kt │ │ │ │ │ └── Decoder.kt │ │ │ │ └── DataType.kt │ │ │ └── MinecraftVersion.kt │ │ ├── string │ │ │ ├── StringUtil.kt │ │ │ └── StringExtension.kt │ │ ├── component │ │ │ ├── ComponentExtension.kt │ │ │ ├── builder │ │ │ │ ├── minimessage │ │ │ │ │ ├── MiniMessageBuilder.kt │ │ │ │ │ └── tag │ │ │ │ │ │ ├── TranslatableBuilder.kt │ │ │ │ │ │ └── PlaceholdersBuilder.kt │ │ │ │ ├── style │ │ │ │ │ ├── ClickEventBuilder.kt │ │ │ │ │ └── StyleBuilder.kt │ │ │ │ ├── text │ │ │ │ │ ├── TextReplacementsBuilder.kt │ │ │ │ │ └── ComponentBuilder.kt │ │ │ │ └── TitleBuilder.kt │ │ │ ├── Format.kt │ │ │ ├── widget │ │ │ │ └── Paginator.kt │ │ │ └── ComponentSerializer.kt │ │ ├── reflect │ │ │ └── ReflectExtension.kt │ │ ├── net │ │ │ ├── http │ │ │ │ ├── HttpUtil.kt │ │ │ │ ├── HttpStatus.kt │ │ │ │ └── YggdrasilApiUtil.kt │ │ │ └── netty │ │ │ │ └── ByteBufExtension.kt │ │ ├── DateTimeUtil.kt │ │ ├── file │ │ │ ├── FileExtension.kt │ │ │ └── FileUtil.kt │ │ ├── collections │ │ │ └── CollectionsExtension.kt │ │ ├── data │ │ │ └── UUIDSerializer.kt │ │ └── server │ │ │ ├── ProxyServerUtil.kt │ │ │ └── ProxyServerExtension.kt │ │ ├── module │ │ ├── command │ │ │ └── session │ │ │ │ ├── ExecuteResult.kt │ │ │ │ ├── Action.kt │ │ │ │ └── CommandSessionManager.kt │ │ ├── whitelist │ │ │ ├── result │ │ │ │ ├── RemoveResult.kt │ │ │ │ └── AddResult.kt │ │ │ ├── data │ │ │ │ └── Player.kt │ │ │ ├── PlayerCache.kt │ │ │ └── WhitelistHandler.kt │ │ ├── imports │ │ │ ├── importers │ │ │ │ ├── Importer.kt │ │ │ │ ├── QuAnVelocityWhitelistImporter.kt │ │ │ │ └── LlsManagerImporter.kt │ │ │ └── PluginName.kt │ │ ├── chatbridge │ │ │ ├── PassthroughMode.kt │ │ │ ├── ChatBridge.kt │ │ │ └── ChatMessage.kt │ │ ├── Logging.kt │ │ ├── mapsync │ │ │ ├── WorldInfoHandler.kt │ │ │ └── XaeroMapHandler.kt │ │ ├── TabSyncHandler.kt │ │ ├── reconnect │ │ │ ├── ReconnectHandler.kt │ │ │ └── Reconnection.kt │ │ └── EventBroadcast.kt │ │ ├── config │ │ ├── data │ │ │ ├── Version.kt │ │ │ ├── TabSync.kt │ │ │ ├── Utility.kt │ │ │ ├── MapSync.kt │ │ │ ├── Reconnect.kt │ │ │ ├── Whitelist.kt │ │ │ └── Broadcast.kt │ │ └── AVMConfig.kt │ │ ├── patch │ │ ├── transformers │ │ │ ├── ClassTransformer.kt │ │ │ └── KeyedChatHandlerTransformer.kt │ │ └── Patch.kt │ │ ├── command │ │ ├── whitelist │ │ │ ├── OffCommand.kt │ │ │ ├── StatusCommand.kt │ │ │ ├── OnCommand.kt │ │ │ ├── ListCommand.kt │ │ │ ├── ClearCommand.kt │ │ │ ├── FindCommand.kt │ │ │ ├── AddCommand.kt │ │ │ └── RemoveCommand.kt │ │ ├── utility │ │ │ ├── KickCommand.kt │ │ │ ├── ImportCommand.kt │ │ │ ├── SendCommand.kt │ │ │ ├── KickAllCommand.kt │ │ │ └── SendAllCommand.kt │ │ ├── AVMCommand.kt │ │ └── chatbridge │ │ │ └── MsgCommand.kt │ │ ├── packet │ │ ├── s2c │ │ │ └── PlayerAbilitiesPacket.kt │ │ └── SetDefaultSpawnPositionPacket.kt │ │ └── i18n │ │ └── TranslateManager.kt │ └── resources │ └── velocity-plugin.json ├── gradle.properties ├── .gitignore ├── .idea └── dictionaries │ └── project.xml ├── LICENSE ├── README_CN.md └── gradlew.bat /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "AdvancedVelocityManager" 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSDNicrosoft/AdvancedVelocityManager/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/annotations/dsl/ComponentDSL.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.annotations.dsl 2 | 3 | @DslMarker 4 | annotation class ComponentDSL 5 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/data/server/Server.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.data.server 2 | 3 | @JvmInline 4 | value class Server(val name: String) 5 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/packet/data/codec/Encoder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.packet.data.codec 2 | 3 | interface Encoder { 4 | fun encode(): E 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/packet/data/codec/Decoder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.packet.data.codec 2 | 3 | interface Decoder { 4 | fun decode(data: E): D 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/annotations/dsl/CommandDSL.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.annotations.dsl 2 | 3 | @Target(AnnotationTarget.TYPE) 4 | @DslMarker 5 | annotation class CommandDSL 6 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/command/session/ExecuteResult.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.command.session 2 | 3 | enum class ExecuteResult { SUCCESS, EXPIRED, FAILED, NOT_FOUND } 4 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/config/data/Version.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.config.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Version(val version: Int) 7 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=work.msdnicrosoft.avm 2 | version=1.5.0 3 | 4 | kotlin.code.style=official 5 | kotlin.incremental=true 6 | kotlin.incremental.java=true 7 | kotlin.caching.enabled=true 8 | org.gradle.caching=true 9 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/whitelist/result/RemoveResult.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.whitelist.result 2 | 3 | enum class RemoveResult { 4 | SUCCESS, 5 | FAIL_NOT_FOUND, 6 | SAVE_FILE_FAILED 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/data/PlayerByUUID.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.data 2 | 3 | import com.velocitypowered.api.proxy.Player 4 | 5 | @JvmInline 6 | value class PlayerByUUID(val player: Player) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/string/StringUtil.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.string 2 | 3 | object StringUtil { 4 | val URL_PATTERN = Regex("^(https?|http?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]") 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/annotations/command/RootCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.annotations.command 2 | 3 | @Retention(AnnotationRetention.RUNTIME) 4 | @Target(AnnotationTarget.CLASS) 5 | annotation class RootCommand(val name: String) 6 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/data/component/MiniMessage.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.data.component 2 | 3 | import net.kyori.adventure.text.Component 4 | 5 | @JvmInline 6 | value class MiniMessage(val component: Component) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/packet/data/DataType.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.packet.data 2 | 3 | import work.msdnicrosoft.avm.util.packet.data.codec.Encoder 4 | 5 | interface DataType : Encoder { 6 | override fun encode(): E 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/ComponentExtension.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component 2 | 3 | import net.kyori.adventure.text.Component 4 | import java.util.Optional 5 | 6 | fun Optional.orEmpty(): Component = this.orElse(Component.empty()) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/whitelist/result/AddResult.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.whitelist.result 2 | 3 | enum class AddResult { 4 | SUCCESS, 5 | API_LOOKUP_NOT_FOUND, 6 | API_LOOKUP_REQUEST_FAILED, 7 | ALREADY_EXISTS, 8 | SAVE_FILE_FAILED, 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/annotations/command/CommandNode.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.annotations.command 2 | 3 | @Retention(AnnotationRetention.RUNTIME) 4 | @Target(AnnotationTarget.FIELD) 5 | annotation class CommandNode( 6 | val name: String, 7 | vararg val arguments: String 8 | ) 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/patch/transformers/ClassTransformer.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.patch.transformers 2 | 3 | import java.lang.instrument.ClassFileTransformer 4 | 5 | interface ClassTransformer : ClassFileTransformer { 6 | val targetClass: Class<*> 7 | fun shouldTransform(): Boolean 8 | } 9 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/data/server/ServerGroup.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.data.server 2 | 3 | import work.msdnicrosoft.avm.config.ConfigManager 4 | 5 | @JvmInline 6 | value class ServerGroup(val name: String) { 7 | val servers: List get() = ConfigManager.config.whitelist.getServersInGroup(this.name) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/velocity-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "advancedvelocitymanager", 3 | "name": "AdvancedVelocityManager", 4 | "main": "work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin", 5 | "version": "${version}", 6 | "description": "AdvancedVelocityManager is a modern and advanced Velocity plugin", 7 | "url": "https://github.com/MSDNicrosoft/AdvancedVelocityManager", 8 | "authors": [ 9 | "MSDNicrosoft" 10 | ] 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/reflect/ReflectExtension.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.reflect 2 | 3 | import com.highcapable.kavaref.extension.classOf 4 | import java.lang.reflect.AnnotatedElement 5 | 6 | inline fun AnnotatedElement.getAnnotationIfPresent(): A? = 7 | try { 8 | getAnnotation(classOf()) 9 | } catch (_: NullPointerException) { 10 | null 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/net/http/HttpUtil.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.net.http 2 | 3 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 4 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 5 | 6 | object HttpUtil { 7 | val USER_AGENT = 8 | "AdvancedVelocityManager/${plugin.self.version.get()} (${server.version.name}/${server.version.version})" 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/command/session/Action.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.command.session 2 | 3 | /** 4 | * An action [block] to be executed in a command session. 5 | * 6 | * The action expires after a specified [expirationTime]. 7 | */ 8 | data class Action(private val block: () -> T, val expirationTime: Long) { 9 | fun isExpired(): Boolean = System.currentTimeMillis() > this.expirationTime 10 | 11 | fun execute(): T = block.invoke() 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/imports/importers/Importer.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.imports.importers 2 | 3 | import work.msdnicrosoft.avm.util.command.context.CommandContext 4 | 5 | interface Importer { 6 | 7 | val displayName: String 8 | 9 | /** 10 | * Import data from other plugins 11 | * 12 | * @param defaultServer The default server to send players to if they are not online 13 | * @return Whether the import was successful 14 | */ 15 | fun import(context: CommandContext, defaultServer: String): Boolean 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/DateTimeUtil.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util 2 | 3 | import java.time.LocalDateTime 4 | import java.time.ZoneId 5 | import java.time.format.DateTimeFormatter 6 | 7 | object DateTimeUtil { 8 | 9 | /** 10 | * Gets the current date and time as a formatted string based on the specified [format] and [time zone][zoneId]. 11 | */ 12 | fun getDateTime(format: String = "yyyy-MM-dd HH:mm:ss", zoneId: ZoneId = ZoneId.systemDefault()): String = 13 | LocalDateTime.now(zoneId).format(DateTimeFormatter.ofPattern(format)) 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/builder/Command.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.builder 2 | 3 | import com.mojang.brigadier.builder.ArgumentBuilder 4 | import com.mojang.brigadier.tree.CommandNode 5 | import com.velocitypowered.api.command.CommandSource 6 | 7 | interface Command { 8 | val node: ArgumentBuilder 9 | 10 | fun build(): CommandNode 11 | 12 | companion object { 13 | const val SINGLE_SUCCESS: Int = com.mojang.brigadier.Command.SINGLE_SUCCESS 14 | const val ILLEGAL_ARGUMENT: Int = -2 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/file/FileExtension.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.file 2 | 3 | import java.io.File 4 | import java.nio.file.Path 5 | import kotlin.io.path.bufferedReader 6 | import kotlin.io.path.bufferedWriter 7 | 8 | fun Path.readTextWithBuffer(): String = this.bufferedReader().use { it.readText() } 9 | fun File.readTextWithBuffer(): String = this.bufferedReader().use { it.readText() } 10 | 11 | fun Path.writeTextWithBuffer(text: String) = this.bufferedWriter().use { it.write(text) } 12 | fun File.writeTextWithBuffer(text: String) = this.bufferedWriter().use { it.write(text) } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/builder/LiteralCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.builder 2 | 3 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 4 | import com.mojang.brigadier.tree.LiteralCommandNode 5 | import com.velocitypowered.api.command.CommandSource 6 | 7 | class LiteralCommand(root: String) : Command { 8 | override val node: LiteralArgumentBuilder = LiteralArgumentBuilder.literal(root) 9 | override fun build(): LiteralCommandNode = this.node.build() 10 | } 11 | 12 | fun literalCommand(literal: String, block: LiteralCommand.() -> Unit) = LiteralCommand(literal).apply(block) 13 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/chatbridge/PassthroughMode.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.chatbridge 2 | 3 | /** 4 | * The modes of passthrough for chat messages. 5 | */ 6 | enum class PassthroughMode { 7 | /** 8 | * All chat messages will be sent to the backend servers. 9 | */ 10 | ALL, 11 | 12 | /** 13 | * No chat messages will be sent to the backend servers. 14 | */ 15 | NONE, 16 | 17 | /** 18 | * If they match one of the patterns, 19 | * chat messages will be sent to the backend server. 20 | */ 21 | PATTERN; 22 | 23 | companion object { 24 | fun of(mode: String): PassthroughMode = valueOf(mode.uppercase()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/string/StringExtension.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.string 2 | 3 | import com.velocitypowered.api.util.UuidUtils 4 | import java.util.* 5 | 6 | /** 7 | * Repeats a string [n] times. 8 | */ 9 | operator fun String.times(n: Int): String = this.repeat(n) 10 | 11 | /** 12 | * Checks if a string is a valid UUID. 13 | */ 14 | fun String.isUuid(): Boolean = runCatching { this.toUuid() }.isSuccess 15 | 16 | /** 17 | * Converts a string to a UUID. 18 | */ 19 | fun String.toUuid(): UUID = UuidUtils.fromUndashed(this.replace("-", "")) 20 | 21 | /** 22 | * Checks if the string is a valid URL. 23 | */ 24 | fun String.isValidUrl(): Boolean = StringUtil.URL_PATTERN.matches(this) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | run/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### IntelliJ IDEA ### 9 | .idea/modules.xml 10 | .idea/jarRepositories.xml 11 | .idea/compiler.xml 12 | .idea/libraries/ 13 | *.iws 14 | *.iml 15 | *.ipr 16 | out/ 17 | !**/src/main/**/out/ 18 | !**/src/test/**/out/ 19 | 20 | ### Eclipse ### 21 | .apt_generated 22 | .classpath 23 | .factorypath 24 | .project 25 | .settings 26 | .springBeans 27 | .sts4-cache 28 | bin/ 29 | !**/src/main/**/bin/ 30 | !**/src/test/**/bin/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | 42 | ### Mac OS ### 43 | .DS_Store -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/file/FileUtil.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.file 2 | 3 | import com.charleskorn.kaml.AmbiguousQuoteStyle 4 | import com.charleskorn.kaml.Yaml 5 | import com.charleskorn.kaml.YamlConfiguration 6 | import com.moandjiezana.toml.Toml 7 | import kotlinx.serialization.json.Json 8 | 9 | object FileUtil { 10 | val YAML: Yaml = Yaml( 11 | configuration = YamlConfiguration( 12 | encodeDefaults = true, 13 | strictMode = false, 14 | ambiguousQuoteStyle = AmbiguousQuoteStyle.DoubleQuoted 15 | ) 16 | ) 17 | 18 | val JSON: Json = Json { 19 | ignoreUnknownKeys = true 20 | prettyPrint = true 21 | } 22 | 23 | val TOML: Toml = Toml() 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/whitelist/data/Player.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.whitelist.data 2 | 3 | import kotlinx.serialization.Serializable 4 | import work.msdnicrosoft.avm.util.data.UUIDSerializer 5 | import java.util.UUID 6 | 7 | /** 8 | * A player in the whitelist. 9 | * 10 | * @property name The name of the player. 11 | * @property uuid The UUID of the player. 12 | * @property onlineMode Whether the player is online mode 13 | * @property serverList The list of servers the player is allowed to connect to. 14 | */ 15 | @Serializable 16 | data class Player( 17 | var name: String, 18 | @Serializable(with = UUIDSerializer::class) 19 | val uuid: UUID, 20 | var onlineMode: Boolean, 21 | var serverList: List 22 | ) 23 | -------------------------------------------------------------------------------- /.idea/dictionaries/project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | advancedvelocitymanager 5 | avmwl 6 | bilibili 7 | cancellablechat 8 | endswith 9 | grgit 10 | kickall 11 | lcom 12 | lorg 13 | modrinth 14 | msdnicrosoft 15 | nicrosoft 16 | nogit 17 | quickshop 18 | sendall 19 | signedvelocity 20 | startswith 21 | takeneko 22 | velocitywhitelist 23 | worldinfo 24 | xaero 25 | xaerominimap 26 | xaeroworldmap 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/collections/CollectionsExtension.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.collections 2 | 3 | /** 4 | * Adds an [element] to the list if the given condition [predicate] is met. 5 | * 6 | * @receiver The list to which the element will be added. 7 | */ 8 | inline fun MutableList.addIf(element: E, predicate: () -> Boolean) { 9 | if (predicate()) { 10 | this.add(element) 11 | } 12 | } 13 | 14 | /** 15 | * Adds a collection of [elements] to the list if the given condition [predicate] is met. 16 | * 17 | * @receiver The list to which the collection will be added. 18 | */ 19 | inline fun MutableList.addAllIf(elements: Collection, predicate: () -> Boolean) { 20 | if (predicate()) { 21 | this.addAll(elements) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/data/UUIDSerializer.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.data 2 | 3 | import com.velocitypowered.api.util.UuidUtils 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.descriptors.PrimitiveKind 6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 7 | import kotlinx.serialization.descriptors.SerialDescriptor 8 | import kotlinx.serialization.encoding.Decoder 9 | import kotlinx.serialization.encoding.Encoder 10 | import java.util.* 11 | 12 | object UUIDSerializer : KSerializer { 13 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) 14 | override fun serialize(encoder: Encoder, value: UUID) = encoder.encodeString(UuidUtils.toUndashed(value)) 15 | override fun deserialize(decoder: Decoder): UUID = UuidUtils.fromUndashed(decoder.decodeString()) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/whitelist/PlayerCache.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.whitelist 2 | 3 | import work.msdnicrosoft.avm.config.ConfigManager 4 | 5 | /** 6 | * This object is used to provide a completion source for the `/avmwl add` command by caching player information. 7 | */ 8 | object PlayerCache { 9 | private inline val config get() = ConfigManager.config.whitelist.cachePlayers 10 | 11 | private val players: MutableSet = mutableSetOf() 12 | 13 | val readOnly: List get() = this.players.toList() 14 | 15 | fun reload() { 16 | if (config.enabled) this.players.clear() 17 | } 18 | 19 | fun add(player: String) { 20 | if (!config.enabled) return 21 | 22 | if (this.players.size >= config.maxSize) { 23 | this.players.remove(this.players.last()) 24 | } 25 | this.players.add(player) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/CommandExtension.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command 2 | 3 | import com.mojang.brigadier.tree.LiteralCommandNode 4 | import com.velocitypowered.api.command.BrigadierCommand 5 | import com.velocitypowered.api.command.CommandMeta 6 | import com.velocitypowered.api.command.CommandSource 7 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.commandManager 8 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 9 | 10 | fun LiteralCommandNode.register(vararg aliases: String) { 11 | val command = BrigadierCommand(this) 12 | val meta: CommandMeta = commandManager.metaBuilder(command) 13 | .aliases(*aliases) 14 | .plugin(plugin) 15 | .build() 16 | commandManager.register(meta, command) 17 | } 18 | 19 | fun LiteralCommandNode.unregister() = commandManager.unregister(this.name) 20 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/config/data/TabSync.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.config.data 2 | 3 | import com.charleskorn.kaml.YamlComment 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class TabSync( 8 | @YamlComment("Whether to enable tab synchronization") 9 | var enabled: Boolean = true, 10 | 11 | @YamlComment( 12 | "The display format for each player in tab list", 13 | "", 14 | "Default: [] ", 15 | "", 16 | "Available placeholders:", 17 | " - The username of a player who sent a message", 18 | " - The name of the server where a player sent a message", 19 | " - The nickname of the server where a player sent a message", 20 | ) 21 | val format: String = "[] " 22 | ) 23 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/config/data/Utility.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.config.data 2 | 3 | import com.charleskorn.kaml.YamlComment 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Utility( 9 | @YamlComment("The `sendall` command configuration") 10 | @SerialName("sendall") 11 | val sendAll: SendAll = SendAll(), 12 | 13 | @YamlComment("The `kickall` command configuration") 14 | @SerialName("kickall") 15 | val kickAll: KickAll = KickAll() 16 | ) { 17 | @Serializable 18 | data class SendAll( 19 | @YamlComment("Allow players who have permission avm.kickall.bypass to bypass the send") 20 | @SerialName("allow-bypass") 21 | val allowBypass: Boolean = true 22 | ) 23 | 24 | @Serializable 25 | data class KickAll( 26 | @YamlComment("Allow players who have permission avm.kickall.bypass to bypass the kick") 27 | @SerialName("allow-bypass") 28 | val allowBypass: Boolean = true 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/imports/PluginName.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.imports 2 | 3 | import work.msdnicrosoft.avm.module.imports.importers.LlsManagerImporter 4 | import work.msdnicrosoft.avm.module.imports.importers.QuAnVelocityWhitelistImporter 5 | import work.msdnicrosoft.avm.util.command.context.CommandContext 6 | 7 | enum class PluginName { 8 | LLS_MANAGER { 9 | override fun import(context: CommandContext, defaultServer: String): Boolean = 10 | LlsManagerImporter.import(context, defaultServer) 11 | }, 12 | QU_AN_VELOCITYWHITELIST { 13 | override fun import(context: CommandContext, defaultServer: String): Boolean = 14 | QuAnVelocityWhitelistImporter.import(context, defaultServer) 15 | }; 16 | 17 | abstract fun import(context: CommandContext, defaultServer: String): Boolean 18 | 19 | companion object { 20 | val PLUGINS: List by lazy { entries.map { it.name.replace("_", "-").lowercase() } } 21 | 22 | fun from(name: String): PluginName = valueOf(name.replace("-", "_").uppercase()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: Ask a question 3 | title: "[Question] " 4 | labels: 5 | - "type: question" 6 | - "resolution: unresolved" 7 | - "status: awaiting response" 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: "**Note: Please fill this question truthfully, otherwise your issue may be closed, locked or deleted directly.**" 12 | 13 | - type: checkboxes 14 | id: before_asking 15 | attributes: 16 | label: Before asking 17 | options: 18 | - label: I have known and agreed that I would fill this question truthfully, or my issue may be closed, locked or deleted unconditionally. 19 | required: true 20 | - label: I have searched for existing issues (including `Open` and `Closed`). 21 | required: true 22 | - label: I am using the latest CI build of AdvancedVelocityManager. 23 | required: false 24 | 25 | - type: textarea 26 | id: description 27 | attributes: 28 | label: Description 29 | description: Tell us your question. 30 | validations: 31 | required: true 32 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/builder/ArgumentCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.builder 2 | 3 | import com.mojang.brigadier.arguments.ArgumentType 4 | import com.mojang.brigadier.builder.RequiredArgumentBuilder 5 | import com.mojang.brigadier.suggestion.Suggestions 6 | import com.mojang.brigadier.suggestion.SuggestionsBuilder 7 | import com.mojang.brigadier.tree.ArgumentCommandNode 8 | import com.velocitypowered.api.command.CommandSource 9 | import work.msdnicrosoft.avm.util.command.context.CommandContext 10 | import java.util.concurrent.CompletableFuture 11 | 12 | class ArgumentCommand(root: String, type: ArgumentType) : Command { 13 | override val node: RequiredArgumentBuilder = RequiredArgumentBuilder.argument(root, type) 14 | override fun build(): ArgumentCommandNode = this.node.build() 15 | 16 | fun suggests(block: CommandContext.(builder: SuggestionsBuilder) -> CompletableFuture) { 17 | this.node.suggests { context, builder -> 18 | CommandContext(context).block(builder) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 MSDNicrosoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/Build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | paths: 6 | - "*.gradle" 7 | - "*.gradle.kts" 8 | - "gradle.properties" 9 | - "src/**" 10 | - "gradle/**" 11 | - "config/**" 12 | - ".github/workflows/*" 13 | pull_request: 14 | workflow_dispatch: 15 | 16 | jobs: 17 | Build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v6 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup Java JDK 26 | uses: actions/setup-java@v5 27 | with: 28 | distribution: "zulu" 29 | java-version: 21 30 | 31 | - name: Setup Gradle 32 | uses: gradle/actions/setup-gradle@v5 33 | with: 34 | validate-wrappers: true 35 | allow-snapshot-wrappers: true 36 | 37 | - name: Build with Gradle 38 | run: gradle build 39 | 40 | - name: Upload Build Artifact 41 | uses: actions/upload-artifact@v6 42 | with: 43 | name: AdvancedVelocityManager-Plugin 44 | path: | 45 | build/libs/*.jar 46 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/whitelist/OffCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.whitelist 2 | 3 | import work.msdnicrosoft.avm.config.ConfigManager 4 | import work.msdnicrosoft.avm.util.command.builder.Command 5 | import work.msdnicrosoft.avm.util.command.builder.executes 6 | import work.msdnicrosoft.avm.util.command.builder.literalCommand 7 | import work.msdnicrosoft.avm.util.command.builder.requires 8 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 9 | 10 | object OffCommand { 11 | private inline val config get() = ConfigManager.config.whitelist 12 | 13 | val command = literalCommand("off") { 14 | requires { hasPermission("avm.command.whitelist.off") } 15 | executes { 16 | config.enabled = false 17 | if (!ConfigManager.save()) { 18 | sendTranslatable("avm.general.config.save.failed") 19 | return@executes Command.SINGLE_SUCCESS 20 | } 21 | sendTranslatable("avm.command.avmwl.status.state") { 22 | args { component("state", tr("avm.general.off")) } 23 | } 24 | Command.SINGLE_SUCCESS 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/server/ProxyServerUtil.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.server 2 | 3 | import com.velocitypowered.api.proxy.Player 4 | import com.velocitypowered.api.proxy.server.ServerPing 5 | import net.kyori.adventure.text.Component 6 | import work.msdnicrosoft.avm.util.component.builder.minimessage.miniMessage 7 | 8 | object ProxyServerUtil { 9 | /** 10 | * A server ping result with an unknown version and description. 11 | */ 12 | val TIMEOUT_PING_RESULT: ServerPing = ServerPing.builder() 13 | .version(ServerPing.Version(-1, "Unknown")) 14 | .description(Component.text("Unknown")) 15 | .build() 16 | 17 | /** 18 | * Kicks a list of [players] from the server with a specified [reason]. 19 | */ 20 | fun kickPlayers(reason: String, vararg players: Player) { 21 | kickPlayers(reason, players.toList()) 22 | } 23 | 24 | /** 25 | * Kicks a list of [players] from the server with a specified [reason]. 26 | */ 27 | fun kickPlayers(reason: String, players: Iterable) { 28 | players.forEach { player -> 29 | player.disconnect(miniMessage(reason)) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/context/CommandSourceExtension.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package work.msdnicrosoft.avm.util.command.context 4 | 5 | import com.velocitypowered.api.command.CommandSource 6 | import com.velocitypowered.api.proxy.ConsoleCommandSource 7 | import com.velocitypowered.api.proxy.Player 8 | import com.velocitypowered.proxy.connection.client.ConnectedPlayer 9 | import net.kyori.adventure.text.JoinConfiguration 10 | import work.msdnicrosoft.avm.util.component.builder.text.ComponentBuilder 11 | import work.msdnicrosoft.avm.util.component.builder.text.component 12 | 13 | inline val CommandSource.isConsole: Boolean get() = this is ConsoleCommandSource 14 | inline val CommandSource.isPlayer: Boolean get() = this is Player 15 | inline val CommandSource.name: String get() = if (this is Player) this.username else "Console" 16 | 17 | fun CommandSource.toPlayer(): Player = this as Player 18 | fun CommandSource.toConsole(): ConsoleCommandSource = this as ConsoleCommandSource 19 | fun CommandSource.toConnectedPlayer(): ConnectedPlayer = this as ConnectedPlayer 20 | 21 | inline fun CommandSource.sendMessage( 22 | joinConfiguration: JoinConfiguration = JoinConfiguration.noSeparators(), 23 | componentBuilder: ComponentBuilder.() -> Unit 24 | ) = this.sendMessage(component(joinConfiguration, componentBuilder)) 25 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/builder/minimessage/MiniMessageBuilder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component.builder.minimessage 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.kyori.adventure.text.minimessage.MiniMessage 5 | import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver 6 | import work.msdnicrosoft.avm.annotations.dsl.ComponentDSL 7 | import work.msdnicrosoft.avm.util.component.ComponentSerializer 8 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.PlaceholdersBuilder 9 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.placeholders as placeholdersBuilder 10 | 11 | @ComponentDSL 12 | class MiniMessageBuilder(val text: String, private val provider: MiniMessage) { 13 | private val placeholders: MutableList = mutableListOf() 14 | 15 | fun placeholders(builder: PlaceholdersBuilder.() -> Unit) { 16 | this.placeholders.addAll(placeholdersBuilder(builder)) 17 | } 18 | 19 | fun build(): Component = this.provider.deserialize(this.text, TagResolver.resolver(this.placeholders)) 20 | } 21 | 22 | inline fun miniMessage( 23 | text: String, 24 | provider: ComponentSerializer = ComponentSerializer.MINI_MESSAGE, 25 | builder: MiniMessageBuilder.() -> Unit = {} 26 | ): Component = MiniMessageBuilder(text, provider.serializer as MiniMessage).apply(builder).build() 27 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/builder/style/ClickEventBuilder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component.builder.style 2 | 3 | import net.kyori.adventure.text.event.ClickEvent 4 | import work.msdnicrosoft.avm.annotations.dsl.ComponentDSL 5 | import java.net.URL 6 | 7 | @Suppress("unused") 8 | @ComponentDSL 9 | class ClickEventBuilder { 10 | private var clickEvent: ClickEvent? = null 11 | 12 | fun fromEvent(event: ClickEvent?) { 13 | this.clickEvent = event 14 | } 15 | 16 | fun openUrl(url: String?) { 17 | if (url == null) return 18 | this.clickEvent = ClickEvent.openUrl(url) 19 | } 20 | 21 | fun openUrl(url: URL?) { 22 | if (url == null) return 23 | this.clickEvent = ClickEvent.openUrl(url) 24 | } 25 | 26 | fun runCommand(command: String?) { 27 | if (command == null) return 28 | this.clickEvent = ClickEvent.runCommand(command) 29 | } 30 | 31 | fun suggestCommand(command: String?) { 32 | if (command == null) return 33 | this.clickEvent = ClickEvent.suggestCommand(command) 34 | } 35 | 36 | fun copyToClipboard(text: String?) { 37 | if (text == null) return 38 | this.clickEvent = ClickEvent.copyToClipboard(text) 39 | } 40 | 41 | fun build(): ClickEvent? = this.clickEvent 42 | } 43 | 44 | inline fun clickEvent(builder: ClickEventBuilder.() -> Unit): ClickEvent? = ClickEventBuilder().apply(builder).build() 45 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/config/data/MapSync.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.config.data 2 | 3 | import com.charleskorn.kaml.YamlComment 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class MapSync( 9 | @YamlComment("Xaero's Minimap and World Map sync settings.") 10 | val xaero: Xaero = Xaero(), 11 | 12 | @YamlComment( 13 | "World info sync settings.", 14 | "This provides support for VoxelMap and JourneyMap.", 15 | ) 16 | @SerialName("world-info") 17 | val worldInfo: WorldInfo = WorldInfo(), 18 | ) { 19 | @Serializable 20 | data class Xaero( 21 | @YamlComment("Whether to enable Xaero's Minimap synchronization.") 22 | val mini: Boolean = true, 23 | @YamlComment("Whether to enable Xaero's WorldMap synchronization.") 24 | val world: Boolean = true, 25 | ) 26 | 27 | @Serializable 28 | data class WorldInfo( 29 | @YamlComment( 30 | "Whether to enable world info synchronization in legacy packet protocol.", 31 | "This is scheduled to be removed in the future.", 32 | ) 33 | val legacy: Boolean = true, 34 | 35 | @YamlComment( 36 | "Whether to enable world info synchronization in modern packet protocol.", 37 | "This is required for higher version of VoxelMap and JourneyMap to work properly.", 38 | ) 39 | val modern: Boolean = true, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/server/ProxyServerExtension.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.server 2 | 3 | import com.velocitypowered.api.proxy.Player 4 | import com.velocitypowered.api.proxy.server.RegisteredServer 5 | import com.velocitypowered.api.proxy.server.ServerInfo 6 | import com.velocitypowered.api.scheduler.ScheduledTask 7 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 8 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.scheduler 9 | import work.msdnicrosoft.avm.config.ConfigManager 10 | import java.util.concurrent.CompletableFuture 11 | import kotlin.time.Duration 12 | import kotlin.time.toJavaDuration 13 | 14 | inline val ServerInfo.nickname: String get() = ConfigManager.config.getServerNickName(this.name) 15 | 16 | /** 17 | * Creates a scheduled [task][runnable] with optional [delay] and [repeat] intervals. 18 | */ 19 | fun task(delay: Duration = Duration.ZERO, repeat: Duration = Duration.ZERO, runnable: Runnable): ScheduledTask { 20 | val taskBuilder = scheduler.buildTask(plugin, runnable) 21 | 22 | if (delay > Duration.ZERO) { 23 | taskBuilder.delay(delay.toJavaDuration()) 24 | } 25 | 26 | if (repeat > Duration.ZERO) { 27 | taskBuilder.repeat(delay.toJavaDuration()) 28 | } 29 | 30 | return taskBuilder.schedule() 31 | } 32 | 33 | /** 34 | * Sends a player to a specific [server]. 35 | */ 36 | fun Player.sendToServer(server: RegisteredServer): CompletableFuture = 37 | this.createConnectionRequest(server).connectWithIndication() 38 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/Format.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * A format for a component. 7 | * 8 | * @property text The text of the component. 9 | * @property hover The hover text of the component. 10 | * @property command The command to run when clicked. 11 | * @property suggest The suggest command to run when clicked. 12 | * @property url The URL to open when clicked. 13 | * @property clipboard The text to copy to the clipboard when clicked. 14 | */ 15 | @Serializable 16 | data class Format( 17 | val text: String, 18 | val hover: List? = null, 19 | val command: String? = null, 20 | val suggest: String? = null, 21 | val url: String? = null, 22 | val clipboard: String? = null 23 | ) { 24 | init { 25 | require(this.text.isNotBlank()) { "Text cannot be empty or blank." } 26 | 27 | val click: List = listOfNotNull(this.command, this.suggest, this.url, this.clipboard) 28 | 29 | require(click.size <= 1) { "Cannot specify multiple actions for a format." } 30 | require(click.all { it.isNotBlank() }) { "Cannot specify empty or blank actions for a format." } 31 | } 32 | 33 | fun applyReplace(replacer: String.() -> String): Format = Format( 34 | this.text.replacer(), 35 | this.hover?.map { it.replacer() }, 36 | this.command?.replacer(), 37 | this.suggest?.replacer(), 38 | this.url?.replacer(), 39 | this.clipboard?.replacer() 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/whitelist/StatusCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.whitelist 2 | 3 | import work.msdnicrosoft.avm.config.ConfigManager 4 | import work.msdnicrosoft.avm.module.whitelist.PlayerCache 5 | import work.msdnicrosoft.avm.module.whitelist.WhitelistManager 6 | import work.msdnicrosoft.avm.util.command.builder.Command 7 | import work.msdnicrosoft.avm.util.command.builder.executes 8 | import work.msdnicrosoft.avm.util.command.builder.literalCommand 9 | import work.msdnicrosoft.avm.util.command.builder.requires 10 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 11 | 12 | object StatusCommand { 13 | private inline val config get() = ConfigManager.config.whitelist 14 | 15 | val command = literalCommand("status") { 16 | requires { hasPermission("avm.command.whitelist.status") } 17 | executes { 18 | val state = if (config.enabled) "on" else "off" 19 | sendTranslatable("avm.command.avmwl.list.header") { 20 | args { numeric("player", WhitelistManager.size) } 21 | } 22 | sendTranslatable("avm.command.avmwl.status.state") { 23 | args { component("state", tr("avm.general.$state")) } 24 | } 25 | sendTranslatable("avm.command.avmwl.status.cache") { 26 | args { 27 | numeric("current", PlayerCache.readOnly.size) 28 | numeric("total", config.cachePlayers.maxSize) 29 | } 30 | } 31 | Command.SINGLE_SUCCESS 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/builder/text/TextReplacementsBuilder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component.builder.text 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.kyori.adventure.text.JoinConfiguration 5 | import net.kyori.adventure.text.TextReplacementConfig 6 | import work.msdnicrosoft.avm.annotations.dsl.ComponentDSL 7 | 8 | @Suppress("unused") 9 | @ComponentDSL 10 | class TextReplacementsBuilder(private val parent: TextReplacementsBuilder? = null) { 11 | var override: Boolean = false 12 | 13 | private val replacements: MutableList = mutableListOf() 14 | 15 | fun replacement(builder: TextReplacementConfig.Builder.() -> Unit) { 16 | this.replacements.add(TextReplacementConfig.builder().apply(builder).build()) 17 | } 18 | 19 | internal fun replace(component: Component): Component { 20 | var replacer: TextReplacementsBuilder = this 21 | var currentComponent: Component = component 22 | do { 23 | currentComponent = replacer.replacements.fold(currentComponent) { curr, config -> 24 | curr.replaceText(config) 25 | } 26 | replacer = replacer.parent ?: TextReplacementsBuilder() 27 | } while (!replacer.override && replacer.parent != null) 28 | return currentComponent 29 | } 30 | } 31 | 32 | inline fun TextReplacementConfig.Builder.replace(crossinline builder: ComponentBuilder.(original: Component) -> Unit) { 33 | this.replacement { replaceBuilder -> 34 | component(JoinConfiguration.noSeparators()) { builder(replaceBuilder.build()) } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/builder/minimessage/tag/TranslatableBuilder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component.builder.minimessage.tag 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.kyori.adventure.text.ComponentLike 5 | import net.kyori.adventure.text.minimessage.translation.Argument 6 | import work.msdnicrosoft.avm.annotations.dsl.ComponentDSL 7 | 8 | @ComponentDSL 9 | class TranslatableBuilder(private val key: String) { 10 | private val arguments: MutableList = mutableListOf() 11 | 12 | fun args(builder: Arguments.() -> Unit) { 13 | this.arguments.addAll(Arguments().apply(builder).build()) 14 | } 15 | 16 | fun build(): Component = Component.translatable(this.key, this.arguments) 17 | 18 | @Suppress("unused") 19 | class Arguments { 20 | private val args: MutableList = mutableListOf() 21 | 22 | fun bool(name: String, value: Boolean) { 23 | this.args.add(Argument.bool(name, value)) 24 | } 25 | 26 | fun numeric(name: String, value: Number) { 27 | this.args.add(Argument.numeric(name, value)) 28 | } 29 | 30 | fun string(name: String, value: String) { 31 | this.args.add(Argument.string(name, value)) 32 | } 33 | 34 | fun component(name: String, value: ComponentLike) { 35 | this.args.add(Argument.component(name, value)) 36 | } 37 | 38 | internal fun build(): List = this.args 39 | } 40 | } 41 | 42 | inline fun tr(key: String, builder: TranslatableBuilder.() -> Unit = {}): Component = 43 | TranslatableBuilder(key).apply(builder).build() 44 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/utility/KickCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.utility 2 | 3 | import com.velocitypowered.api.proxy.Player 4 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 5 | import work.msdnicrosoft.avm.util.command.builder.* 6 | import work.msdnicrosoft.avm.util.command.context.name 7 | import work.msdnicrosoft.avm.util.command.data.component.MiniMessage 8 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 9 | import work.msdnicrosoft.avm.util.server.task 10 | 11 | object KickCommand { 12 | val command = literalCommand("kick") { 13 | requires { hasPermission("avm.command.kick") } 14 | wordArgument("player") { 15 | suggests { builder -> 16 | server.allPlayers.forEach { builder.suggest(it.username) } 17 | builder.buildFuture() 18 | } 19 | executes { 20 | val player: Player by this 21 | 22 | task { 23 | player.disconnect( 24 | tr("avm.command.avm.kick.target") { 25 | args { string("executor", context.source.name) } 26 | } 27 | ) 28 | } 29 | Command.SINGLE_SUCCESS 30 | } 31 | stringArgument("reason") { 32 | executes { 33 | val player: Player by this 34 | val reason: MiniMessage by this 35 | task { player.disconnect(reason.component) } 36 | Command.SINGLE_SUCCESS 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/packet/s2c/PlayerAbilitiesPacket.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.packet.s2c 2 | 3 | import com.velocitypowered.api.network.ProtocolVersion 4 | import com.velocitypowered.proxy.connection.MinecraftSessionHandler 5 | import com.velocitypowered.proxy.protocol.MinecraftPacket 6 | import com.velocitypowered.proxy.protocol.ProtocolUtils 7 | import io.netty.buffer.ByteBuf 8 | 9 | class PlayerAbilitiesPacket( 10 | flags: List, 11 | val flyingSpeed: Float = 0.05F, 12 | val fieldOfViewModifier: Float = 0.1F 13 | ) : MinecraftPacket { 14 | constructor() : this(EMPTY) 15 | 16 | private val flags: Int = flags.fold(0) { acc: Int, flag: Flag -> acc or flag.value } 17 | 18 | override fun decode(buf: ByteBuf, direction: ProtocolUtils.Direction, protocolVersion: ProtocolVersion) = 19 | error("PlayerAbilitiesPacket should not be decoded.") 20 | 21 | override fun encode(buf: ByteBuf, direction: ProtocolUtils.Direction, protocolVersion: ProtocolVersion) { 22 | buf.writeByte(this.flags) 23 | buf.writeFloat(this.flyingSpeed) 24 | buf.writeFloat(this.fieldOfViewModifier) 25 | } 26 | 27 | override fun handle(sessionHandler: MinecraftSessionHandler?): Boolean = true 28 | 29 | companion object { 30 | @Suppress("MagicNumber", "unused") 31 | enum class Flag(val value: Int) { 32 | EMPTY(0x00), 33 | INVULNERABLE(0x01), 34 | FLYING(0x02), 35 | ALLOW_FLYING(0x04), 36 | CREATIVE_MODE(0x08) 37 | } 38 | 39 | val NO_FALLING: List = listOf(Flag.FLYING, Flag.ALLOW_FLYING) 40 | val EMPTY: List = listOf(Flag.EMPTY) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/packet/MinecraftVersion.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.packet 2 | 3 | import com.velocitypowered.api.network.ProtocolVersion 4 | 5 | @Suppress("unused") 6 | enum class MinecraftVersion { 7 | UNKNOWN, 8 | LEGACY, 9 | MINECRAFT_1_7_2, 10 | MINECRAFT_1_7_6, 11 | MINECRAFT_1_8, 12 | MINECRAFT_1_9, 13 | MINECRAFT_1_9_1, 14 | MINECRAFT_1_9_2, 15 | MINECRAFT_1_9_4, 16 | MINECRAFT_1_10, 17 | MINECRAFT_1_11, 18 | MINECRAFT_1_11_1, 19 | MINECRAFT_1_12, 20 | MINECRAFT_1_12_1, 21 | MINECRAFT_1_12_2, 22 | MINECRAFT_1_13, 23 | MINECRAFT_1_13_1, 24 | MINECRAFT_1_13_2, 25 | MINECRAFT_1_14, 26 | MINECRAFT_1_14_1, 27 | MINECRAFT_1_14_2, 28 | MINECRAFT_1_14_3, 29 | MINECRAFT_1_14_4, 30 | MINECRAFT_1_15, 31 | MINECRAFT_1_15_1, 32 | MINECRAFT_1_15_2, 33 | MINECRAFT_1_16, 34 | MINECRAFT_1_16_1, 35 | MINECRAFT_1_16_2, 36 | MINECRAFT_1_16_3, 37 | MINECRAFT_1_16_4, 38 | MINECRAFT_1_17, 39 | MINECRAFT_1_17_1, 40 | MINECRAFT_1_18, 41 | MINECRAFT_1_18_2, 42 | MINECRAFT_1_19, 43 | MINECRAFT_1_19_1, 44 | MINECRAFT_1_19_3, 45 | MINECRAFT_1_19_4, 46 | MINECRAFT_1_20, 47 | MINECRAFT_1_20_2, 48 | MINECRAFT_1_20_3, 49 | MINECRAFT_1_20_5, 50 | MINECRAFT_1_21, 51 | MINECRAFT_1_21_2, 52 | MINECRAFT_1_21_4, 53 | MINECRAFT_1_21_5, 54 | MINECRAFT_1_21_6, 55 | MINECRAFT_1_21_7, 56 | MINECRAFT_1_21_9; 57 | 58 | companion object { 59 | fun MinecraftVersion.toProtocolVersion(): ProtocolVersion? = try { 60 | ProtocolVersion.valueOf(this.name.uppercase()) 61 | } catch (_: IllegalArgumentException) { 62 | null 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/whitelist/OnCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.whitelist 2 | 3 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 4 | import work.msdnicrosoft.avm.config.ConfigManager 5 | import work.msdnicrosoft.avm.module.whitelist.WhitelistManager 6 | import work.msdnicrosoft.avm.util.command.builder.Command 7 | import work.msdnicrosoft.avm.util.command.builder.executes 8 | import work.msdnicrosoft.avm.util.command.builder.literalCommand 9 | import work.msdnicrosoft.avm.util.command.builder.requires 10 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 11 | import work.msdnicrosoft.avm.util.server.ProxyServerUtil.kickPlayers 12 | import work.msdnicrosoft.avm.util.server.task 13 | 14 | object OnCommand { 15 | private inline val config get() = ConfigManager.config.whitelist 16 | 17 | val command = literalCommand("on") { 18 | requires { hasPermission("avm.command.whitelist.on") } 19 | executes { 20 | config.enabled = true 21 | if (!ConfigManager.save()) { 22 | sendTranslatable("avm.general.config.save.failed") 23 | return@executes Command.SINGLE_SUCCESS 24 | } 25 | sendTranslatable("avm.command.avmwl.status.state") { 26 | args { component("state", tr("avm.general.on")) } 27 | } 28 | task { 29 | kickPlayers( 30 | config.message, 31 | if (WhitelistManager.isEmpty) { 32 | server.allPlayers 33 | } else { 34 | server.allPlayers.filter { it.uniqueId !in WhitelistManager.uuids } 35 | } 36 | ) 37 | } 38 | Command.SINGLE_SUCCESS 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/whitelist/ListCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.whitelist 2 | 3 | import work.msdnicrosoft.avm.command.WhitelistCommand.sendWhitelistPlayers 4 | import work.msdnicrosoft.avm.module.whitelist.WhitelistManager 5 | import work.msdnicrosoft.avm.util.command.builder.* 6 | import work.msdnicrosoft.avm.util.command.context.CommandContext 7 | import work.msdnicrosoft.avm.util.component.widget.Paginator 8 | 9 | object ListCommand { 10 | val command = literalCommand("list") { 11 | requires { hasPermission("avm.command.whitelist.list") } 12 | executes { 13 | listWhitelist(1) 14 | Command.SINGLE_SUCCESS 15 | } 16 | intArgument("page", min = 1) { 17 | suggests { builder -> 18 | for (page: Int in 1..WhitelistManager.maxPage) builder.suggest(page) 19 | builder.buildFuture() 20 | } 21 | executes { 22 | val page: Int by this 23 | listWhitelist(page) 24 | Command.SINGLE_SUCCESS 25 | } 26 | } 27 | } 28 | 29 | private fun CommandContext.listWhitelist(page: Int) { 30 | if (WhitelistManager.isEmpty) { 31 | sendTranslatable("avm.command.avmwl.list.empty") 32 | return 33 | } 34 | val maxPage: Int = WhitelistManager.maxPage 35 | if (page > maxPage) { 36 | sendTranslatable("avm.general.not_found.page") 37 | return 38 | } 39 | if (page == 1) { 40 | sendTranslatable("avm.command.avmwl.list.header") { 41 | args { numeric("player", WhitelistManager.size) } 42 | } 43 | } 44 | 45 | context.source.sendWhitelistPlayers(WhitelistManager.pageOf(page)) 46 | 47 | sendMessage(Paginator("/avmwl list").toComponent(page, maxPage)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/Logging.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module 2 | 3 | import com.velocitypowered.api.event.Subscribe 4 | import com.velocitypowered.api.event.proxy.ProxyShutdownEvent 5 | import com.velocitypowered.api.scheduler.ScheduledTask 6 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.dataDirectory 7 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.eventManager 8 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.logger 9 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 10 | import work.msdnicrosoft.avm.util.DateTimeUtil 11 | import work.msdnicrosoft.avm.util.server.task 12 | import java.io.File 13 | import kotlin.io.path.div 14 | import kotlin.time.Duration.Companion.minutes 15 | 16 | object Logging { 17 | private val file: File get() = (dataDirectory / "logs" / "${DateTimeUtil.getDateTime("yyyy-MM-dd")}.log").toFile() 18 | 19 | private val messages: MutableList = mutableListOf() 20 | 21 | private lateinit var writeTask: ScheduledTask 22 | 23 | fun init() { 24 | eventManager.register(plugin, this) 25 | this.writeTask = task(repeat = 5.minutes, runnable = this::write) 26 | } 27 | 28 | @Suppress("UnusedParameter") 29 | @Subscribe 30 | fun onProxyShutdown(event: ProxyShutdownEvent) { 31 | this.write() 32 | } 33 | 34 | private fun write() { 35 | if (this.messages.isEmpty()) return 36 | 37 | try { 38 | this.file.bufferedWriter().use { writer -> 39 | this.messages.forEach { message -> 40 | writer.appendLine(message) 41 | } 42 | } 43 | this.messages.clear() 44 | } catch (e: Exception) { 45 | logger.warn("Failed to write log file: {}", e.message) 46 | } 47 | } 48 | 49 | fun log(message: String) = this.messages.add("[${DateTimeUtil.getDateTime("HH:mm:ss")}]$message") 50 | } 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project. 3 | title: "[Feature Request] " 4 | labels: 5 | - "type: enhancement" 6 | - "resolution: unresolved" 7 | - "status: awaiting response" 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: "**Note: Please fill this request truthfully, otherwise your issue may be closed, locked or deleted directly.**" 12 | 13 | - type: checkboxes 14 | id: before_requesting 15 | attributes: 16 | label: Before requesting 17 | options: 18 | - label: I have known and agreed that I would fill this request truthfully, or my issue may be closed, locked or deleted unconditionally. 19 | required: true 20 | - label: I have searched for existing issues (including `Open` and `Closed`). 21 | required: true 22 | - label: I am using the latest CI build of AdvancedVelocityManager. 23 | required: false 24 | 25 | - type: textarea 26 | id: description 27 | attributes: 28 | label: Description of the new features 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: necessity 34 | attributes: 35 | label: The necessity of the new features 36 | description: Why do you think the new features or new changes is needed? 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | id: relevant_problems 42 | attributes: 43 | label: The problems related to the new features 44 | description: Is your feature request related to a problem? Please describe. 45 | placeholder: A clear and concise description of what the problem is. 46 | validations: 47 | required: false 48 | 49 | - type: textarea 50 | id: additional_information 51 | attributes: 52 | label: Additional information 53 | description: Add any other context or screenshots about the feature request here. 54 | validations: 55 | required: false 56 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/context/CommandContext.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.context 2 | 3 | import com.highcapable.kavaref.extension.classOf 4 | import com.velocitypowered.api.command.CommandSource 5 | import net.kyori.adventure.text.ComponentLike 6 | import net.kyori.adventure.text.JoinConfiguration 7 | import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver 8 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.TranslatableBuilder 9 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 10 | import work.msdnicrosoft.avm.util.component.builder.text.ComponentBuilder 11 | import work.msdnicrosoft.avm.util.component.builder.text.component 12 | import kotlin.reflect.KProperty 13 | import com.mojang.brigadier.context.CommandContext as BrigadierCommandContext 14 | 15 | @Suppress("unused") 16 | class CommandContext(val context: BrigadierCommandContext) { 17 | inline operator fun getValue(thisRef: Any?, property: KProperty<*>): T { 18 | val parser: ArgumentParser = ArgumentParser.of() 19 | ?: return this.context.getArgument(property.name, classOf()) 20 | return parser.parse(this.context.getArgument(property.name, String::class.java)) 21 | } 22 | 23 | fun sendMessage(message: ComponentLike) = this.context.source.sendMessage(message) 24 | 25 | inline fun sendMessage( 26 | joinConfiguration: JoinConfiguration = JoinConfiguration.spaces(), 27 | componentBuilder: ComponentBuilder.() -> Unit 28 | ) = this.sendMessage(component(joinConfiguration, componentBuilder)) 29 | 30 | fun sendPlainMessage(message: String) = this.context.source.sendPlainMessage(message) 31 | 32 | fun sendRichMessage(message: String, vararg resolvers: TagResolver) = 33 | this.context.source.sendRichMessage(message, *resolvers) 34 | 35 | fun sendTranslatable(key: String, builder: TranslatableBuilder.() -> Unit = {}) = this.sendMessage(tr(key, builder)) 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/builder/TitleBuilder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component.builder 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.kyori.adventure.text.JoinConfiguration 5 | import net.kyori.adventure.title.Title 6 | import work.msdnicrosoft.avm.annotations.dsl.ComponentDSL 7 | import work.msdnicrosoft.avm.util.component.builder.text.ComponentBuilder 8 | import work.msdnicrosoft.avm.util.component.builder.text.component 9 | import kotlin.time.Duration 10 | import kotlin.time.toJavaDuration 11 | 12 | @ComponentDSL 13 | class TitleBuilder { 14 | private var mainTitle: Component = Component.empty() 15 | private var subTitle: Component = Component.empty() 16 | 17 | private var fadeIn: Duration = Duration.ZERO 18 | private var stay: Duration = Duration.ZERO 19 | private var fadeOut: Duration = Duration.ZERO 20 | 21 | fun mainTitle( 22 | joinConfiguration: JoinConfiguration = JoinConfiguration.noSeparators(), 23 | componentBuilder: ComponentBuilder.() -> Unit 24 | ) { 25 | this.mainTitle = component(joinConfiguration, componentBuilder) 26 | } 27 | 28 | fun subTitle( 29 | joinConfiguration: JoinConfiguration = JoinConfiguration.noSeparators(), 30 | componentBuilder: ComponentBuilder.() -> Unit 31 | ) { 32 | this.subTitle = component(joinConfiguration, componentBuilder) 33 | } 34 | 35 | fun fadeIn(duration: Duration) { 36 | this.fadeIn = duration 37 | } 38 | 39 | fun stay(duration: Duration) { 40 | this.stay = duration 41 | } 42 | 43 | fun fadeOut(duration: Duration) { 44 | this.fadeOut = duration 45 | } 46 | 47 | fun build(): Title = Title.title( 48 | this.mainTitle, 49 | this.subTitle, 50 | Title.Times.times( 51 | this.fadeIn.toJavaDuration(), 52 | this.stay.toJavaDuration(), 53 | this.fadeOut.toJavaDuration() 54 | ) 55 | ) 56 | } 57 | 58 | inline fun title(builder: TitleBuilder.() -> Unit): Title = TitleBuilder().apply(builder).build() 59 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/builder/minimessage/tag/PlaceholdersBuilder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component.builder.minimessage.tag 2 | 3 | import net.kyori.adventure.text.ComponentLike 4 | import net.kyori.adventure.text.format.StyleBuilderApplicable 5 | import net.kyori.adventure.text.minimessage.Context 6 | import net.kyori.adventure.text.minimessage.tag.Tag 7 | import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue 8 | import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder 9 | import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver 10 | import work.msdnicrosoft.avm.annotations.dsl.ComponentDSL 11 | 12 | @Suppress("unused") 13 | @ComponentDSL 14 | class PlaceholdersBuilder { 15 | private val placeholders: MutableList = mutableListOf() 16 | 17 | fun parsed(key: String, value: String) { 18 | this.placeholders.add(Placeholder.parsed(key, value)) 19 | } 20 | 21 | fun unparsed(key: String, value: String) { 22 | this.placeholders.add(Placeholder.unparsed(key, value)) 23 | } 24 | 25 | fun numeric(key: String, value: Number) { 26 | this.placeholders.add(Placeholder.parsed(key, value.toString())) 27 | } 28 | 29 | fun component(key: String, value: ComponentLike) { 30 | this.placeholders.add(Placeholder.component(key, value)) 31 | } 32 | 33 | fun styling(key: String, vararg style: StyleBuilderApplicable) { 34 | this.placeholders.add(Placeholder.styling(key, *style)) 35 | } 36 | 37 | fun tagResolver(key: String, handler: (arguments: ArgumentQueue, context: Context) -> Tag) { 38 | this.placeholders.add(TagResolver.resolver(key, handler)) 39 | } 40 | 41 | fun tagResolvers(vararg resolvers: TagResolver) { 42 | this.placeholders.addAll(resolvers) 43 | } 44 | 45 | fun tagResolvers(resolvers: Iterable) { 46 | this.placeholders.addAll(resolvers) 47 | } 48 | 49 | fun build(): List = this.placeholders 50 | } 51 | 52 | inline fun placeholders(builder: PlaceholdersBuilder.() -> Unit): List = 53 | PlaceholdersBuilder().apply(builder).build() 54 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/widget/Paginator.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component.widget 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.kyori.adventure.text.JoinConfiguration 5 | import net.kyori.adventure.text.format.NamedTextColor 6 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 7 | import work.msdnicrosoft.avm.util.component.builder.text.component 8 | import kotlin.math.ceil 9 | 10 | class Paginator(private val command: String) { 11 | 12 | fun toComponent(currentPage: Int, maxPage: Int): Component = component(JoinConfiguration.spaces()) { 13 | componentLike( 14 | button("<-") { 15 | borderType(Button.BorderType.SQUARE) 16 | color { 17 | enabled(NamedTextColor.GOLD) 18 | disabled(NamedTextColor.GRAY) 19 | border(NamedTextColor.DARK_GRAY) 20 | } 21 | enableWhen { currentPage != 1 } 22 | click { whenEnabled { runCommand("${this@Paginator.command} ${currentPage - 1}") } } 23 | hover { whenEnabled(tr("avm.general.page.previous")) } 24 | } 25 | ) 26 | text("$currentPage/$maxPage") styled { color(NamedTextColor.AQUA) } 27 | componentLike( 28 | button("->") { 29 | borderType(Button.BorderType.SQUARE) 30 | color { 31 | enabled(NamedTextColor.GOLD) 32 | disabled(NamedTextColor.GRAY) 33 | border(NamedTextColor.DARK_GRAY) 34 | } 35 | enableWhen { currentPage != maxPage } 36 | click { whenEnabled { runCommand("${this@Paginator.command} ${currentPage + 1}") } } 37 | hover { whenEnabled(tr("avm.general.page.next")) } 38 | } 39 | ) 40 | } 41 | 42 | companion object { 43 | /** 44 | * The number of items to display per page. 45 | */ 46 | const val ITEMS_PER_PAGE = 10 47 | 48 | /** 49 | * Calculates the maximum number of pages for the given [size]. 50 | */ 51 | fun getMaxPage(size: Int): Int = ceil(size.toFloat() / this.ITEMS_PER_PAGE.toFloat()).toInt() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/net/netty/ByteBufExtension.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.net.netty 2 | 3 | import io.netty.buffer.ByteBuf 4 | import io.netty.buffer.ByteBufUtil 5 | 6 | /** 7 | * Executes the given [block] on [this][ByteBuf] and **always** releases it afterward, even if an exception is thrown. 8 | * 9 | * Example usage: 10 | * ``` 11 | * val answer = buffer.use { buf -> 12 | * buf.writeIntLE(42) 13 | * buf.readIntLE() 14 | * } 15 | * // buffer has been released here; answer == 42 16 | * ``` 17 | * 18 | * @return the value returned by [block] 19 | * @throws Exception any exception thrown by [block]; the buffer is still released 20 | */ 21 | inline fun T.use(block: (T) -> R): R = 22 | try { 23 | block(this) 24 | } finally { 25 | this.release() 26 | } 27 | 28 | /** 29 | * Executes the given [block] as a receiver-style lambda on [this][ByteBuf] 30 | * and **always** releases it afterward, even on exception. 31 | * 32 | * Example usage: 33 | * ``` 34 | * buffer.useApply { 35 | * skipBytes(4) 36 | * writeByte(0xFF) 37 | * }.also { released -> 38 | * println("Buffer released? ${released.refCnt() == 0}") // true 39 | * } 40 | * ``` 41 | * 42 | * @return the same [ByteBuf] instance, now with `refCnt == 0` 43 | */ 44 | inline fun T.useApply(block: T.() -> R): T = 45 | try { 46 | this.block() 47 | this 48 | } finally { 49 | this.release() 50 | } 51 | 52 | /** 53 | * Executes the given [block] as a receiver-style lambda on [this][ByteBuf] 54 | * and **always** releases it afterward, even on exception. 55 | * 56 | * 57 | * Example usage: 58 | * ``` 59 | * val crc = buffer.useThenApply { 60 | * val array = ByteArray(readableBytes()) 61 | * readBytes(array) 62 | * CRC32().let { it.update(array); it.value } 63 | * } 64 | * // buffer released, crc contains the checksum 65 | * ``` 66 | * 67 | * @return the value returned by [block] 68 | * @throws Exception any exception thrown by [block]; the buffer is still released 69 | */ 70 | inline fun T.useThenApply(block: T.() -> R): R = 71 | try { 72 | this.block() 73 | } finally { 74 | this.release() 75 | } 76 | 77 | inline fun T.toByteArray(): ByteArray = ByteBufUtil.getBytes(this) 78 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/config/data/Reconnect.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.config.data 2 | 3 | import com.charleskorn.kaml.YamlComment 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Reconnect( 9 | @YamlComment("Whether to enable the reconnect feature") 10 | val enabled: Boolean = false, 11 | 12 | // language=regexp 13 | @YamlComment("Regex pattern to match server shutdown messages triggering reconnect") 14 | val pattern: String = "((?i)^(server closed|server is restarting|multiplayer\\.disconnect\\.server_shutdown))+$", 15 | 16 | @YamlComment("Heartbeat interval in milliseconds for reconnect detection") 17 | @SerialName("ping-interval") 18 | val pingInterval: Long = 1000L, 19 | 20 | @YamlComment("Heartbeat timeout in milliseconds, considered disconnected if exceeded") 21 | @SerialName("ping-timeout") 22 | val pingTimeout: Long = 300L, 23 | 24 | @YamlComment("Interval in milliseconds to refresh reconnect message") 25 | @SerialName("message-interval") 26 | val messageInterval: Long = 1000L, 27 | 28 | @YamlComment("Delay in milliseconds before attempting to reconnect") 29 | @SerialName("reconnect-delay") 30 | val reconnectDelay: Long = 2000L, 31 | 32 | @YamlComment("Reconnect message configuration") 33 | val message: Message = Message() 34 | ) { 35 | @Serializable 36 | data class Message( 37 | @YamlComment("Message shown while waiting to reconnect") 38 | val waiting: Waiting = Waiting(), 39 | @YamlComment("Message shown while connecting") 40 | val connecting: Connecting = Connecting() 41 | ) { 42 | @Serializable 43 | data class Waiting( 44 | @YamlComment("Title shown while waiting to reconnect") 45 | val title: String = "Server Restarting...", 46 | @YamlComment("Subtitle shown while waiting to reconnect") 47 | val subtitle: String = "Please wait a moment before reconnecting." 48 | ) 49 | 50 | @Serializable 51 | data class Connecting( 52 | @YamlComment("Title shown while connecting") 53 | val title: String = "Reconnecting...", 54 | @YamlComment("Subtitle shown while connecting") 55 | val subtitle: String = "Please wait..." 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/whitelist/ClearCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.whitelist 2 | 3 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 4 | import work.msdnicrosoft.avm.config.ConfigManager 5 | import work.msdnicrosoft.avm.module.command.session.CommandSessionManager 6 | import work.msdnicrosoft.avm.module.whitelist.WhitelistManager 7 | import work.msdnicrosoft.avm.util.command.builder.Command 8 | import work.msdnicrosoft.avm.util.command.builder.executes 9 | import work.msdnicrosoft.avm.util.command.builder.literalCommand 10 | import work.msdnicrosoft.avm.util.command.builder.requires 11 | import work.msdnicrosoft.avm.util.command.context.name 12 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 13 | import work.msdnicrosoft.avm.util.component.builder.style.styled 14 | import work.msdnicrosoft.avm.util.server.ProxyServerUtil.kickPlayers 15 | import work.msdnicrosoft.avm.util.server.task 16 | 17 | object ClearCommand { 18 | private inline val config get() = ConfigManager.config.whitelist 19 | 20 | val command = literalCommand("clear") { 21 | requires { hasPermission("avm.command.whitelist.clear") } 22 | executes { 23 | val sessionId: String = CommandSessionManager.generateSessionId( 24 | context.source.name, 25 | System.currentTimeMillis(), 26 | context.arguments.values.joinToString(" ") 27 | ) 28 | 29 | CommandSessionManager.add(sessionId) { 30 | if (WhitelistManager.clear()) { 31 | sendTranslatable("avm.command.avmwl.clear.success") 32 | } else { 33 | sendTranslatable("avm.command.avmwl.clear.failed") 34 | } 35 | if (config.enabled) { 36 | task { kickPlayers(config.message, server.allPlayers) } 37 | } 38 | } 39 | sendTranslatable("avm.command.avmwl.clear.need_confirm.1.text") 40 | sendMessage( 41 | tr("avm.command.avmwl.clear.need_confirm.2.text") { 42 | args { string("command", "/avm confirm $sessionId") } 43 | } styled { 44 | hoverText { tr("avm.command.avmwl.clear.need_confirm.2.hover") } 45 | click { runCommand("/avm confirm $sessionId") } 46 | } 47 | ) 48 | Command.SINGLE_SUCCESS 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # Gradle Plugins 3 | kotlin = "2.3.0" 4 | detekt = "1.23.8" 5 | grgit = "5.3.3" 6 | yamlang = "1.5.0" 7 | shadow = "9.3.0" 8 | 9 | # CompileOnly Dependencies 10 | velocity = "3.4.0-SNAPSHOT" 11 | asm = "9.9.1" 12 | floodgate = "2.2.5-SNAPSHOT" 13 | netty = "4.2.9.Final" 14 | fastutil = "8.5.18" 15 | 16 | # Shade Dependencies 17 | kaml = "0.104.0" 18 | kotlin-serialization = "1.9.0" 19 | byte-buddy-agent = "1.18.2" 20 | kavaref = "1.0.2" 21 | kavaref-extension = "1.0.2" 22 | 23 | 24 | [plugins] 25 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 26 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 27 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } 28 | detekt-compiler = { id = "io.github.detekt.gradle.compiler-plugin", version.ref = "detekt" } 29 | grgit = { id = "org.ajoberstar.grgit", version.ref = "grgit" } 30 | yamlang = { id = "me.fallenbreath.yamlang", version.ref = "yamlang" } 31 | shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } 32 | 33 | 34 | [libraries] 35 | # Detekt Plugins 36 | detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } 37 | 38 | # CompileOnly Dependencies 39 | velocity-api = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" } 40 | velocity-proxy = { module = "com.velocitypowered:velocity-proxy", version.ref = "velocity" } 41 | asm = { module = "org.ow2.asm:asm", version.ref = "asm" } 42 | asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" } 43 | floodgate = { module = "org.geysermc.floodgate:api", version.ref = "floodgate" } 44 | netty = { module = "io.netty:netty-all", version.ref = "netty" } 45 | fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" } 46 | 47 | # Shade Dependencies 48 | kaml = { module = "com.charleskorn.kaml:kaml-jvm", version.ref = "kaml" } 49 | kotlin-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "kotlin-serialization" } 50 | byte-buddy-agent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "byte-buddy-agent" } 51 | kavaref-core = { module = "com.highcapable.kavaref:kavaref-core", version.ref = "kavaref" } 52 | kavaref-extension = { module = "com.highcapable.kavaref:kavaref-extension", version.ref = "kavaref-extension" } 53 | 54 | 55 | [bundles] 56 | asm = ["asm", "asm-util"] 57 | velocity = ["velocity-api", "velocity-proxy"] 58 | kavaref = ["kavaref-core", "kavaref-extension"] 59 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/whitelist/FindCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.whitelist 2 | 3 | import work.msdnicrosoft.avm.command.WhitelistCommand.sendWhitelistPlayers 4 | import work.msdnicrosoft.avm.module.whitelist.PlayerCache 5 | import work.msdnicrosoft.avm.module.whitelist.WhitelistManager 6 | import work.msdnicrosoft.avm.module.whitelist.data.Player 7 | import work.msdnicrosoft.avm.util.command.builder.* 8 | import work.msdnicrosoft.avm.util.command.context.CommandContext 9 | import work.msdnicrosoft.avm.util.component.widget.Paginator 10 | import work.msdnicrosoft.avm.util.server.task 11 | 12 | object FindCommand { 13 | val command = literalCommand("find") { 14 | requires { hasPermission("avm.command.whitelist.find") } 15 | wordArgument("keyword") { 16 | suggests { builder -> 17 | WhitelistManager.usernames.forEach(builder::suggest) 18 | PlayerCache.readOnly.forEach(builder::suggest) 19 | builder.buildFuture() 20 | } 21 | executes { 22 | val keyword: String by this 23 | listFind(1, keyword) 24 | Command.SINGLE_SUCCESS 25 | } 26 | intArgument("page", min = 1) { 27 | executes { 28 | val page: Int by this 29 | val keyword: String by this 30 | task { listFind(page, keyword) } 31 | Command.SINGLE_SUCCESS 32 | } 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Sends a message to the sender with the header for the whitelist find, 39 | * then sends a message for each player found on the specified page. 40 | * Finally, sends a message with the footer for the whitelist find. 41 | * 42 | * @param page The page number to retrieve. 43 | * @param keyword The keyword to search for. 44 | */ 45 | private fun CommandContext.listFind(page: Int, keyword: String) { 46 | val result: List = WhitelistManager.find(keyword, page) 47 | 48 | if (result.isEmpty()) { 49 | sendTranslatable("avm.command.avmwl.find.empty") 50 | return 51 | } 52 | 53 | val maxPage: Int = Paginator.getMaxPage(result.size) 54 | if (page > maxPage) { 55 | sendTranslatable("avm.general.not_found.page") 56 | return 57 | } 58 | 59 | if (page == 1) sendTranslatable("avm.command.avmwl.find.header") 60 | 61 | context.source.sendWhitelistPlayers(result) 62 | sendMessage(Paginator("/avmwl find $keyword").toComponent(page, maxPage)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/config/AVMConfig.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.config 2 | 3 | import com.charleskorn.kaml.YamlComment 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | import work.msdnicrosoft.avm.config.data.* 7 | 8 | @Serializable 9 | data class AVMConfig( 10 | @YamlComment( 11 | "The version of configuration", 12 | "", 13 | "DO NOT CHANGE THIS, OTHERWISE IT MAY CAUSE CRITICAL PROBLEMS" 14 | ) 15 | val version: Int = 2, 16 | 17 | @YamlComment( 18 | "The language for plugin to use", 19 | "The language name must be the part file name of files in the `lang` folder", 20 | "Example:", 21 | " zh_CN.yml -> zh_CN", 22 | " en_US.yml -> en_US" 23 | ) 24 | @SerialName("default-language") 25 | val defaultLang: String = "en_US", 26 | 27 | @YamlComment( 28 | "Mapping of the server names (configured in `velocity.toml`) and nicknames", 29 | "", 30 | "Format: server-name: server-nickname", 31 | "Example:", 32 | " survival: \"Survival\"", 33 | " factions: \"Factions\"", 34 | " minigames: \"Minigames\"" 35 | ) 36 | @SerialName("server-mapping") 37 | val serverMapping: Map = mapOf( 38 | "lobby" to "Lobby", 39 | "factions" to "Factions", 40 | "minigames" to "Minigames" 41 | ), 42 | 43 | @YamlComment("The event broadcast configuration") 44 | val broadcast: Broadcast = Broadcast(), 45 | 46 | @YamlComment("The utility commands configuration") 47 | val utility: Utility = Utility(), 48 | 49 | @YamlComment("The whitelist configuration") 50 | val whitelist: Whitelist = Whitelist(), 51 | 52 | @YamlComment("The Chat Bridge configuration") 53 | @SerialName("chat-bridge") 54 | val chatBridge: ChatBridge = ChatBridge(), 55 | 56 | @YamlComment("The TabList Synchronization configuration") 57 | @SerialName("tab-sync") 58 | val tabSync: TabSync = TabSync(), 59 | 60 | @YamlComment("The Map Synchronization configuration") 61 | @SerialName("map-sync") 62 | val mapSync: MapSync = MapSync(), 63 | 64 | @YamlComment("The Reconnection configuration") 65 | val reconnect: Reconnect = Reconnect(), 66 | ) { 67 | 68 | /** 69 | * Retrieves the server nickname from the serverMapping configuration. 70 | * If no mapping is found for the server, return the original server name. 71 | * 72 | * @param server The server name to retrieve the nickname for. 73 | * @return The server nickname. 74 | */ 75 | fun getServerNickName(server: String): String = this.serverMapping[server] ?: server 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/command/session/CommandSessionManager.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.command.session 2 | 3 | import com.google.common.io.BaseEncoding 4 | import com.velocitypowered.api.scheduler.ScheduledTask 5 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.logger 6 | import work.msdnicrosoft.avm.util.server.task 7 | import java.security.MessageDigest 8 | import java.util.concurrent.ConcurrentHashMap 9 | import kotlin.time.Duration.Companion.minutes 10 | 11 | object CommandSessionManager { 12 | private val SHA256: MessageDigest = MessageDigest.getInstance("SHA-256") 13 | private val BASE32: BaseEncoding = BaseEncoding.base32().omitPadding() 14 | 15 | private val sessions: ConcurrentHashMap> = ConcurrentHashMap() 16 | 17 | /** 18 | * The task responsible for removing expired command sessions. 19 | */ 20 | private lateinit var removalTask: ScheduledTask 21 | 22 | fun init() { 23 | // Every 20 minutes, remove all expired sessions 24 | this.removalTask = task(repeat = 20L.minutes) { 25 | this.sessions.entries.removeIf { it.value.isExpired() } 26 | } 27 | } 28 | 29 | fun disable() { 30 | this.removalTask.cancel() 31 | } 32 | 33 | fun reload() { 34 | logger.info("Reloading command sessions...") 35 | this.disable() 36 | this.sessions.clear() 37 | this.init() 38 | } 39 | 40 | /** 41 | * Adds a command session with [sessionId] and [block] to be executed to the manager. 42 | */ 43 | fun add(sessionId: String, block: () -> T) { 44 | sessions[sessionId] = Action(block = block, expirationTime = System.currentTimeMillis() + 60_000L) 45 | } 46 | 47 | /** 48 | * Executes a specified [sessionId] command session. 49 | * 50 | * @return The result of executing the command session. 51 | */ 52 | fun executeAction(sessionId: String): ExecuteResult { 53 | val action = sessions.remove(sessionId) ?: return ExecuteResult.NOT_FOUND 54 | return try { 55 | if (action.isExpired()) { 56 | ExecuteResult.EXPIRED 57 | } else { 58 | action.execute() 59 | ExecuteResult.SUCCESS 60 | } 61 | } catch (e: Exception) { 62 | logger.warn("Failed to execute session command", e) 63 | ExecuteResult.FAILED 64 | } 65 | } 66 | 67 | /** 68 | * Generates a session ID based on the provided [name], [time], and [command]. 69 | */ 70 | fun generateSessionId(name: String, time: Long, command: String): String { 71 | val digest: ByteArray = SHA256.digest("$name$time$command".toByteArray()) 72 | return BASE32.encode(digest) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/builder/style/StyleBuilder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component.builder.style 2 | 3 | import net.kyori.adventure.key.Key 4 | import net.kyori.adventure.text.Component 5 | import net.kyori.adventure.text.ComponentLike 6 | import net.kyori.adventure.text.event.ClickEvent 7 | import net.kyori.adventure.text.event.HoverEvent 8 | import net.kyori.adventure.text.format.ShadowColor 9 | import net.kyori.adventure.text.format.Style 10 | import net.kyori.adventure.text.format.TextColor 11 | import net.kyori.adventure.text.format.TextDecoration 12 | import work.msdnicrosoft.avm.annotations.dsl.ComponentDSL 13 | import work.msdnicrosoft.avm.util.component.Format 14 | 15 | @Suppress("unused") 16 | @ComponentDSL 17 | class StyleBuilder { 18 | private var style: Style = Style.empty() 19 | 20 | fun color(color: TextColor) { 21 | this.style = this.style.color(color) 22 | } 23 | 24 | fun shadowColor(shadowColor: ShadowColor) { 25 | this.style = this.style.shadowColor(shadowColor) 26 | } 27 | 28 | fun italic(italic: Boolean = true) { 29 | this.style = this.style.decoration(TextDecoration.ITALIC, italic) 30 | } 31 | 32 | fun bold(bold: Boolean = true) { 33 | this.style = this.style.decoration(TextDecoration.BOLD, bold) 34 | } 35 | 36 | fun strikethrough(strikethrough: Boolean = true) { 37 | this.style = this.style.decoration(TextDecoration.STRIKETHROUGH, strikethrough) 38 | } 39 | 40 | fun obfuscated(obfuscated: Boolean = true) { 41 | this.style = this.style.decoration(TextDecoration.OBFUSCATED, obfuscated) 42 | } 43 | 44 | fun font(key: Key) { 45 | this.style = this.style.font(key) 46 | } 47 | 48 | fun hoverText(text: String?) { 49 | if (text == null) return 50 | this.style = this.style.hoverEvent(HoverEvent.showText(Component.text(text))) 51 | } 52 | 53 | fun hoverText(text: ComponentLike?) { 54 | if (text == null) return 55 | this.style = this.style.hoverEvent(HoverEvent.showText(text)) 56 | } 57 | 58 | fun insertion(text: String?) { 59 | if (text == null) return 60 | this.style = this.style.insertion(text) 61 | } 62 | 63 | fun click(builder: ClickEventBuilder.() -> Unit) { 64 | this.style = this.style.clickEvent(clickEvent(builder)) 65 | } 66 | 67 | fun click(format: Format) { 68 | this.click { 69 | runCommand(format.command) 70 | suggestCommand(format.suggest) 71 | openUrl(format.url) 72 | copyToClipboard(format.clipboard) 73 | } 74 | } 75 | 76 | fun fromClickEvent(event: ClickEvent?) { 77 | this.style = this.style.clickEvent(event) 78 | } 79 | 80 | fun build(): Style = this.style 81 | } 82 | 83 | inline fun style(builder: StyleBuilder.() -> Unit): Style = StyleBuilder().apply(builder).build() 84 | 85 | inline infix fun Component.styled(builder: StyleBuilder.() -> Unit): Component = this.style(style(builder)) 86 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/mapsync/WorldInfoHandler.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Portions of this code are modified from lls-manager 3 | * https://github.com/plusls/lls-manager/blob/master/src/main/java/com/plusls/llsmanager/minimapWorldSync/MinimapWorldSyncHandler.java 4 | */ 5 | 6 | package work.msdnicrosoft.avm.module.mapsync 7 | 8 | import com.velocitypowered.api.event.Subscribe 9 | import com.velocitypowered.api.event.connection.PluginMessageEvent 10 | import com.velocitypowered.api.proxy.Player 11 | import com.velocitypowered.api.proxy.ServerConnection 12 | import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier 13 | import io.netty.buffer.Unpooled 14 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.channelRegistrar 15 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.eventManager 16 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 17 | import work.msdnicrosoft.avm.config.ConfigManager 18 | import work.msdnicrosoft.avm.util.net.netty.toByteArray 19 | import work.msdnicrosoft.avm.util.net.netty.useApply 20 | import java.nio.charset.StandardCharsets 21 | 22 | object WorldInfoHandler { 23 | private val WORLD_INFO_CHANNEL: MinecraftChannelIdentifier = 24 | MinecraftChannelIdentifier.create("worldinfo", "world_id") 25 | 26 | private inline val config get() = ConfigManager.config.mapSync.worldInfo 27 | 28 | fun init() { 29 | channelRegistrar.register(this.WORLD_INFO_CHANNEL) 30 | eventManager.register(plugin, this) 31 | } 32 | 33 | fun disable() { 34 | channelRegistrar.unregister(this.WORLD_INFO_CHANNEL) 35 | eventManager.unregisterListener(plugin, this) 36 | } 37 | 38 | @Subscribe 39 | fun onPluginMessage(event: PluginMessageEvent) { 40 | if (!config.modern && !config.legacy) return 41 | if (event.identifier != this.WORLD_INFO_CHANNEL) return 42 | 43 | val player = event.source as? Player ?: return 44 | 45 | player.currentServer.ifPresent { connection: ServerConnection -> 46 | val serverNameBytes: ByteArray = connection.serverInfo.name.toByteArray(StandardCharsets.UTF_8) 47 | if (config.modern) { 48 | player.sendPluginMessage(this.WORLD_INFO_CHANNEL, createArray(serverNameBytes, true)) 49 | } 50 | if (config.legacy) { 51 | player.sendPluginMessage(this.WORLD_INFO_CHANNEL, createArray(serverNameBytes, false)) 52 | } 53 | } 54 | 55 | if (config.modern || config.legacy) { 56 | event.result = PluginMessageEvent.ForwardResult.handled() 57 | } 58 | } 59 | 60 | @Suppress("MagicNumber") 61 | private fun createArray(serverNameBytes: ByteArray, modern: Boolean): ByteArray = 62 | Unpooled.buffer().useApply { 63 | writeByte(0x00) // Packet ID 64 | if (modern) writeByte(0x2A) // New packet 65 | writeByte(serverNameBytes.size) 66 | writeBytes(serverNameBytes) 67 | }.toByteArray() 68 | } 69 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # AdvancedVelocityManager 2 | 3 |
4 | English 5 | | 6 | 简体中文 7 |
8 | 9 | ## 简介 10 | 11 | AdvancedVelocityManager 是一个为 Minecraft Velocity 代理服务器设计的高级管理插件。
12 | 它提供了一整套强大的工具,帮助服务器管理员更高效地进行玩家管理和服务器自动化操作。
13 | 该插件支持玩家在不同服务器间的快速传送、自定义广播消息、与 [Floodgate](https://geysermc.org/wiki/floodgate/) 兼容的精细化白名单管理,以及增强的跨服聊天体验。 14 | 15 | ## 功能亮点 16 | 17 | - **跨服务器发送 (`/avm send` 和 `/avm sendall`)**:将单个或批量玩家从一个服务器发送到另一个服务器,并可选择提供理由。 18 | - **玩家踢出** (`/avm kick` 和 `/avm kickall`): 将单个或所有玩家踢出服务器,并可选择提供理由。 19 | - **与 [Floodgate](https://geysermc.org/wiki/floodgate/) 兼容的白名单管理 (`/avmwl`)**:通过 UUID 和用户名添加/移除玩家白名单,并能够为每个玩家分配特定服务器或服务器组的访问权限。 20 | - **自定义广播**:自定义玩家加入、离开和服务器切换的广播消息。 21 | - **Tab 列表同步**: 实现跨服的 Tab 列表显示一致性,同时支持自定义显示格式。 22 | - **跨服聊天(Chat-Bridge)**: 允许不同服务器间的玩家进行聊天,同时支持自定义聊天格式。 23 | 24 | ## 安装指南 25 | 26 | 1. 下载 AdvancedVelocityManager 插件的最新版本。 27 | 2. 将插件文件放入 Velocity 服务器的 `plugins` 目录中。 28 | 3. 重启 Velocity 服务器以加载插件。 29 | 4. 根据需要编辑 `config.yml` 文件以调整插件设置,然后执行命令 `/avm reload` 以重载插件。 30 | 31 | ## 使用方法 32 | 33 | - **白名单管理**:使用 `/avmwl ...` 命令 34 | - **单个玩家跨服务器发送**:使用 `/avm send <玩家名> <目标服务器>` 命令。 35 | - **批量跨服务器发送**:使用 `/avm sendall <源服务器> <目标服务器>` 命令。 36 | - **玩家踢出**:使用 `/avm kick <玩家名> [理由]` 命令。 37 | - **批量踢出**:使用 `/avm kickall <服务器> [理由]` 命令从指定服务器踢出所有玩家。 38 | 39 | ### 权限 40 | 41 | - `avm.command.info` - 查看插件信息 42 | - `avm.command.reload` - 放弃内存中的数据,并从文件中重载配置、语言和白名单 43 | - `avm.command.confirm` - 确认操作 44 | - `avm.command.import` - 从其它插件([lls-manager](https://github.com/plusls/lls-manager) [VelocityWhitelist](https://gitee.com/virtual-qu-an/velocity-whitelist))导入数据 45 | - `avm.command.kick` - 踢出指定玩家 46 | - `avm.command.send` - 将单个玩家从一个服务器发送到另一个服务器 47 | - `avm.command.sendall` - 将指定服务器的所有玩家发送到另一个服务器 48 | - `avm.command.kickall` - 从指定服务器踢出所有玩家 49 | - `avm.command.whitelist.list` - 查看白名单 50 | - `avm.command.whitelist.add` - 通过用户名或 UUID 将玩家添加到白名单 51 | - `avm.command.whitelist.remove` - 从白名单中移除玩家 52 | - `avm.command.whitelist.clear` - 清空白名单 53 | - `avm.command.whitelist.find` - 通过关键词在白名单内查找玩家 54 | - `avm.command.whitelist.on` - 开启白名单 55 | - `avm.command.whitelist.off` - 关闭白名单 56 | - `avm.command.whitelist.status` - 查看白名单状态 57 | - `avm.sendall.bypass` - 绕过 `/sendall` 命令 58 | - `avm.kickall.bypass` - 绕过 `/kickall` 命令 59 | 60 | ### 配置文件 61 | 62 | 配置文件 `config.yml` 允许您自定义插件的各个方面,包括服务器映射、广播消息、命令配置、白名单设置等。 63 | 64 | ## 致谢 65 | 66 | 本插件的开发受到其他开源项目的启发,包括但不限与 [cancellable-chat](https://github.com/ZhuRuoLing/cancellable-chat) 和 [lls-manager](https://github.com/plusls/lls-manager)。
67 | 我们感谢这些项目对开源社区的贡献。 68 | 69 | ## 支持与社区 70 | 71 | - **问题反馈**:创建 GitHub Issue 报告任何问题或功能请求。 72 | 73 | ## TODO 74 | 75 | - [ ] 发布到 [Modrinth](https://modrinth.com) 76 | - [x] `/msg` 跨服私聊 77 | - [x] TabList 同步 78 | - [ ] 更高级的聊天交互 79 | - [x] 从 lls-manager 导入数据 80 | - [x] 离线模式白名单 81 | - [ ] Web 接口请求管理 (HTTP / gRPC / WebSocket) **[可能废弃]** 82 | - [x] 聊天消息日志输出 83 | 84 | ## 许可与版权 85 | 86 | AdvancedVelocityManager 根据 MIT 许可证 发布。您可以自由使用、修改和分发此插件,但请保留原作者的版权声明。 87 | 88 | ## 贡献 89 | 90 | 我们欢迎对 AdvancedVelocityManager 的贡献。无论是通过代码、错误报告或功能建议,您的输入对我们都很有价值。 91 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/packet/SetDefaultSpawnPositionPacket.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.packet 2 | 3 | import com.highcapable.kavaref.KavaRef.Companion.resolve 4 | import com.highcapable.kavaref.extension.classOf 5 | import com.highcapable.kavaref.resolver.FieldResolver 6 | import com.velocitypowered.api.network.ProtocolVersion 7 | import com.velocitypowered.proxy.connection.MinecraftSessionHandler 8 | import com.velocitypowered.proxy.connection.backend.BackendPlaySessionHandler 9 | import com.velocitypowered.proxy.connection.backend.VelocityServerConnection 10 | import com.velocitypowered.proxy.protocol.MinecraftPacket 11 | import com.velocitypowered.proxy.protocol.ProtocolUtils 12 | import io.netty.buffer.ByteBuf 13 | import io.netty.buffer.Unpooled 14 | import work.msdnicrosoft.avm.config.ConfigManager 15 | import work.msdnicrosoft.avm.module.mapsync.XaeroMapHandler 16 | import work.msdnicrosoft.avm.util.net.netty.toByteArray 17 | import work.msdnicrosoft.avm.util.net.netty.use 18 | import work.msdnicrosoft.avm.util.net.netty.useApply 19 | import java.nio.charset.StandardCharsets 20 | import java.util.zip.CRC32 21 | 22 | class SetDefaultSpawnPositionPacket : MinecraftPacket { 23 | private var data: ByteBuf? = null 24 | 25 | override fun decode(buf: ByteBuf, direction: ProtocolUtils.Direction?, protocolVersion: ProtocolVersion?) { 26 | this.data = buf.readBytes(buf.readableBytes()) 27 | } 28 | 29 | override fun encode(buf: ByteBuf, direction: ProtocolUtils.Direction?, protocolVersion: ProtocolVersion?) { 30 | this.data?.use { buf.writeBytes(this.data) } 31 | } 32 | 33 | @Suppress("UnsafeCallOnNullableType") 34 | override fun handle(sessionHandler: MinecraftSessionHandler): Boolean { 35 | if (!config.world && !config.mini) return true 36 | 37 | val connection: VelocityServerConnection = SERVER_CONN_RESOLVER.copy() 38 | .of(sessionHandler as BackendPlaySessionHandler) 39 | .get()!! 40 | 41 | connection.player.connection.write(this) 42 | 43 | val serverNameBytes: ByteArray = connection.serverInfo.name.toByteArray(StandardCharsets.UTF_8) 44 | val worldId: Int = CRC32().apply { 45 | update(serverNameBytes) 46 | }.value.toInt() 47 | val array: ByteArray = Unpooled.buffer().useApply { 48 | writeByte(0x00) // Packet ID 49 | writeInt(worldId) // World ID 50 | }.toByteArray() 51 | 52 | if (config.world) { 53 | connection.player.sendPluginMessage(XaeroMapHandler.XAERO_WORLD_MAP_CHANNEL, array) 54 | } 55 | if (config.mini) { 56 | connection.player.sendPluginMessage(XaeroMapHandler.XAERO_MINI_MAP_CHANNEL, array) 57 | } 58 | 59 | return true 60 | } 61 | 62 | companion object { 63 | private val SERVER_CONN_RESOLVER: FieldResolver by lazy { 64 | classOf().resolve() 65 | .firstField { name = "serverConn" } 66 | } 67 | 68 | private inline val config get() = ConfigManager.config.mapSync.xaero 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/mapsync/XaeroMapHandler.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Portions of this code are modified from lls-manager 3 | * https://github.com/plusls/lls-manager/blob/master/src/main/java/com/plusls/llsmanager/minimapWorldSync/PlayerSpawnPosition.java 4 | */ 5 | 6 | package work.msdnicrosoft.avm.module.mapsync 7 | 8 | import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier 9 | import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction 10 | import com.velocitypowered.proxy.protocol.StateRegistry 11 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.channelRegistrar 12 | import work.msdnicrosoft.avm.packet.SetDefaultSpawnPositionPacket 13 | import work.msdnicrosoft.avm.util.packet.MinecraftVersion 14 | import work.msdnicrosoft.avm.util.packet.Packet 15 | 16 | object XaeroMapHandler { 17 | val XAERO_MINI_MAP_CHANNEL: MinecraftChannelIdentifier = 18 | MinecraftChannelIdentifier.create("xaerominimap", "main") 19 | 20 | val XAERO_WORLD_MAP_CHANNEL: MinecraftChannelIdentifier = 21 | MinecraftChannelIdentifier.create("xaeroworldmap", "main") 22 | 23 | // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol_version_numbers 24 | // https://minecraft.wiki/w/Java_Edition_protocol/Packets#Set_Default_Spawn_Position 25 | @Suppress("MagicNumber") 26 | private val packet: Packet = Packet.of(SetDefaultSpawnPositionPacket::class) 27 | .direction(Direction.CLIENTBOUND) 28 | .stateRegistry(StateRegistry.PLAY) 29 | .packetSupplier(::SetDefaultSpawnPositionPacket) 30 | .mapping(0x05, MinecraftVersion.MINECRAFT_1_7_2, false) 31 | .mapping(0x43, MinecraftVersion.MINECRAFT_1_9, false) 32 | .mapping(0x45, MinecraftVersion.MINECRAFT_1_12, false) 33 | .mapping(0x46, MinecraftVersion.MINECRAFT_1_12_1, false) 34 | .mapping(0x49, MinecraftVersion.MINECRAFT_1_13, false) 35 | .mapping(0x4D, MinecraftVersion.MINECRAFT_1_14, false) 36 | .mapping(0x4E, MinecraftVersion.MINECRAFT_1_15, false) 37 | .mapping(0x42, MinecraftVersion.MINECRAFT_1_16, false) 38 | .mapping(0x4B, MinecraftVersion.MINECRAFT_1_17, false) 39 | .mapping(0x4A, MinecraftVersion.MINECRAFT_1_19, false) 40 | .mapping(0x4D, MinecraftVersion.MINECRAFT_1_19_1, false) 41 | .mapping(0x4C, MinecraftVersion.MINECRAFT_1_19_3, false) 42 | .mapping(0x50, MinecraftVersion.MINECRAFT_1_19_4, false) 43 | .mapping(0x52, MinecraftVersion.MINECRAFT_1_20_2, false) 44 | .mapping(0x54, MinecraftVersion.MINECRAFT_1_20_3, false) 45 | .mapping(0x56, MinecraftVersion.MINECRAFT_1_20_5, false) 46 | .mapping(0x5B, MinecraftVersion.MINECRAFT_1_21_4, false) 47 | .mapping(0x5A, MinecraftVersion.MINECRAFT_1_21_5, false) 48 | 49 | fun init() { 50 | this.packet.register() 51 | channelRegistrar.register(this.XAERO_WORLD_MAP_CHANNEL, this.XAERO_MINI_MAP_CHANNEL) 52 | } 53 | 54 | fun disable() { 55 | this.packet.unregister() 56 | channelRegistrar.unregister(this.XAERO_WORLD_MAP_CHANNEL, this.XAERO_MINI_MAP_CHANNEL) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/patch/Patch.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Portions of this code are modified from cancellable-chat and are licensed under the MIT License (MIT). 3 | * 4 | * https://github.com/ZhuRuoLing/cancellable-chat/blob/977f1dfef71d783b0a824e80ab36ce25d30f2e65 5 | * /src/main/java/icu/takeneko/cancellablechat/InstrumentationAccess.java 6 | * 7 | * Copyright (c) 2024 竹若泠 8 | * 9 | * MIT License 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy 12 | * of this software and associated documentation files (the "Software"), to deal 13 | * in the Software without restriction, including without limitation the rights 14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the Software is 16 | * furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in all 19 | * copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | * SOFTWARE. 28 | */ 29 | 30 | package work.msdnicrosoft.avm.patch 31 | 32 | import net.bytebuddy.agent.ByteBuddyAgent 33 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.logger 34 | import work.msdnicrosoft.avm.patch.transformers.ClassTransformer 35 | import work.msdnicrosoft.avm.patch.transformers.KeyedChatHandlerTransformer 36 | import java.lang.instrument.Instrumentation 37 | import java.lang.management.ManagementFactory 38 | 39 | object Patch { 40 | private val transformers: Set = setOf( 41 | KeyedChatHandlerTransformer, 42 | ) 43 | 44 | private lateinit var instrumentation: Instrumentation 45 | 46 | fun init() { 47 | try { 48 | val toTransform: List = this.transformers.filter { it.shouldTransform() } 49 | 50 | if (toTransform.isEmpty()) return 51 | 52 | this.instrumentation = ByteBuddyAgent.install() 53 | this.warnIfDynamicAgentDisabled() 54 | 55 | toTransform.forEach { transformer -> 56 | this.instrumentation.addTransformer(transformer, true) 57 | this.instrumentation.retransformClasses(transformer.targetClass) 58 | } 59 | } catch (e: Exception) { 60 | logger.error("Failed to initialize Patch", e) 61 | } 62 | } 63 | 64 | private fun warnIfDynamicAgentDisabled() { 65 | if ("-XX:+EnableDynamicAgentLoading" !in ManagementFactory.getRuntimeMXBean().inputArguments) { 66 | logger.info("Dynamic agent loading warnings detected.") 67 | logger.info("It is expected behavior and you can safely ignore the warnings.") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/builder/CommandDSL.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package work.msdnicrosoft.avm.util.command.builder 4 | 5 | import com.mojang.brigadier.arguments.* 6 | import com.velocitypowered.api.command.CommandSource 7 | import work.msdnicrosoft.avm.annotations.dsl.CommandDSL 8 | import work.msdnicrosoft.avm.util.command.context.CommandContext 9 | 10 | fun Command.stringArgument(name: String, block: @CommandDSL ArgumentCommand.() -> Unit) { 11 | this.node.then(ArgumentCommand(name, StringArgumentType.string()).apply(block).node) 12 | } 13 | 14 | fun Command.wordArgument(name: String, block: @CommandDSL ArgumentCommand.() -> Unit) { 15 | this.node.then(ArgumentCommand(name, StringArgumentType.word()).apply(block).node) 16 | } 17 | 18 | fun Command.greedyStringArgument(name: String, block: @CommandDSL ArgumentCommand.() -> Unit) { 19 | this.node.then(ArgumentCommand(name, StringArgumentType.greedyString()).apply(block).node) 20 | } 21 | 22 | fun Command.boolArgument(name: String, block: @CommandDSL ArgumentCommand.() -> Unit) { 23 | this.node.then(ArgumentCommand(name, BoolArgumentType.bool()).apply(block).node) 24 | } 25 | 26 | fun Command.intArgument( 27 | name: String, 28 | min: Int = Int.MIN_VALUE, 29 | max: Int = Int.MAX_VALUE, 30 | block: @CommandDSL ArgumentCommand.() -> Unit 31 | ) { 32 | this.node.then(ArgumentCommand(name, IntegerArgumentType.integer(min, max)).apply(block).node) 33 | } 34 | 35 | fun Command.longArgument( 36 | name: String, 37 | min: Long = Long.MIN_VALUE, 38 | max: Long = Long.MAX_VALUE, 39 | block: @CommandDSL ArgumentCommand.() -> Unit 40 | ) { 41 | this.node.then(ArgumentCommand(name, LongArgumentType.longArg(min, max)).apply(block).node) 42 | } 43 | 44 | fun Command.floatArgument( 45 | name: String, 46 | min: Float = -Float.MAX_VALUE, 47 | max: Float = Float.MAX_VALUE, 48 | block: @CommandDSL ArgumentCommand.() -> Unit 49 | ) { 50 | this.node.then(ArgumentCommand(name, FloatArgumentType.floatArg(min, max)).apply(block).node) 51 | } 52 | 53 | fun Command.doubleArgument( 54 | name: String, 55 | min: Double = -Double.MAX_VALUE, 56 | max: Double = Double.MAX_VALUE, 57 | block: @CommandDSL ArgumentCommand.() -> Unit 58 | ) { 59 | this.node.then(ArgumentCommand(name, DoubleArgumentType.doubleArg(min, max)).apply(block).node) 60 | } 61 | 62 | fun Command.requires(requirement: @CommandDSL CommandSource.() -> Boolean) { 63 | this.node.requires(requirement) 64 | } 65 | 66 | fun Command.executes(block: @CommandDSL CommandContext.() -> Int) { 67 | this.node.executes { CommandContext(it).block() } 68 | } 69 | 70 | fun Command.then(command: LiteralCommand) { 71 | this.node.then(command.node) 72 | } 73 | 74 | fun Command.then(command: ArgumentCommand) { 75 | this.node.then(command.node) 76 | } 77 | 78 | fun Command.literal(literal: String, block: LiteralCommand.() -> Unit) { 79 | this.node.then(LiteralCommand(literal).apply(block).node) 80 | } 81 | 82 | fun Command.argument(name: String, type: ArgumentType, block: ArgumentCommand.() -> Unit) { 83 | this.node.then(ArgumentCommand(name, type).apply(block).node) 84 | } 85 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/config/data/Whitelist.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.config.data 2 | 3 | import com.charleskorn.kaml.YamlComment 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Whitelist( 9 | @YamlComment("Whether to enable whitelist") 10 | var enabled: Boolean = false, 11 | 12 | @YamlComment("The server groups to add/remove whitelist bulky") 13 | @SerialName("server-groups") 14 | var serverGroups: Map> = mapOf( 15 | "Default" to listOf("lobby"), 16 | "Games" to listOf("factions", "minigames") 17 | ), 18 | 19 | @YamlComment("The message sent to a not whitelisted player") 20 | val message: String = "You are not whitelisted on this server.", 21 | 22 | @YamlComment( 23 | "The API URLs to query for whitelist", 24 | "", 25 | "DO NOT MODIFY THIS PART OF CONFIGURATION", 26 | "IF YOU DO NOT KNOW WHAT YOU ARE DOING!!!", 27 | ) 28 | @SerialName("query-api-url") 29 | val queryApi: QueryApi = QueryApi(), 30 | 31 | @YamlComment( 32 | "Cache not-whitelisted players who attempted to join server", 33 | "This provides extra Username completion source for command `/avmwl add`" 34 | ) 35 | @SerialName("cache-players") 36 | val cachePlayers: CachePlayers = CachePlayers() 37 | ) { 38 | @Serializable 39 | data class QueryApi( 40 | @YamlComment( 41 | "The API URL to query UUID by username", 42 | "", 43 | "Default: https://api.minecraftservices.com/minecraft/profile/lookup/name/", 44 | "", 45 | "Learn more: https://minecraft.wiki/w/Mojang_API#Query_player's_UUID" 46 | ) 47 | var uuid: String = "https://api.minecraftservices.com/minecraft/profile/lookup/name/", 48 | 49 | @YamlComment( 50 | "The API URL to query username by UUID", 51 | "", 52 | "Default: https://api.minecraftservices.com/minecraft/profile/lookup/", 53 | "", 54 | "Learn more: https://minecraft.wiki/w/Mojang_API#Query_player's_username" 55 | ) 56 | var profile: String = "https://api.minecraftservices.com/minecraft/profile/lookup/", 57 | ) 58 | 59 | @Serializable 60 | data class CachePlayers( 61 | @YamlComment("Whether to enable cache players") 62 | var enabled: Boolean = true, 63 | 64 | @YamlComment( 65 | "The max size of the cache", 66 | "", 67 | "Default: 20" 68 | ) 69 | @SerialName("max-size") 70 | var maxSize: Int = 20 71 | ) 72 | 73 | /** 74 | * Checks if a server with the given name belongs to a server group. 75 | * 76 | * @param name The name of the server to check. 77 | * @return True if the server belongs to a server group, false otherwise. 78 | */ 79 | fun isServerGroup(name: String): Boolean = name in this.serverGroups.keys 80 | 81 | /** 82 | * Retrieves a list of servers that belong to the specified group. 83 | * 84 | * @param groupName The name of the server group to retrieve servers for. 85 | */ 86 | fun getServersInGroup(groupName: String): List = this.serverGroups[groupName].orEmpty() 87 | } 88 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/TabSyncHandler.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module 2 | 3 | import com.velocitypowered.api.event.Subscribe 4 | import com.velocitypowered.api.event.connection.DisconnectEvent 5 | import com.velocitypowered.api.event.player.ServerPostConnectEvent 6 | import com.velocitypowered.api.proxy.Player 7 | import com.velocitypowered.api.proxy.player.TabListEntry 8 | import com.velocitypowered.api.proxy.server.ServerInfo 9 | import net.kyori.adventure.text.Component 10 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.eventManager 11 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 12 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 13 | import work.msdnicrosoft.avm.config.ConfigManager 14 | import work.msdnicrosoft.avm.util.component.builder.minimessage.miniMessage 15 | import work.msdnicrosoft.avm.util.server.nickname 16 | import work.msdnicrosoft.avm.util.server.task 17 | 18 | object TabSyncHandler { 19 | private inline val config get() = ConfigManager.config.tabSync 20 | 21 | fun init() { 22 | eventManager.register(plugin, this) 23 | } 24 | 25 | fun disable() { 26 | eventManager.unregisterListener(plugin, this) 27 | } 28 | 29 | @Subscribe 30 | fun onPlayerDisconnect(event: DisconnectEvent) { 31 | if (!config.enabled) return 32 | 33 | task { 34 | server.allPlayers.forEach { player -> 35 | player.tabList.removeEntry(event.player.uniqueId) 36 | } 37 | } 38 | } 39 | 40 | @Subscribe 41 | fun onPostPlayerConnected(event: ServerPostConnectEvent) { 42 | if (!config.enabled) return 43 | 44 | task { 45 | val player = event.player 46 | server.allPlayers.forEach { entryPlayer -> 47 | if (entryPlayer != player) { 48 | this@TabSyncHandler.update(player, entryPlayer) 49 | } 50 | this@TabSyncHandler.update(entryPlayer, player) 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Updates the tab list entry of the [target] player with the display name of the [entry] player. 57 | */ 58 | private fun update(target: Player, entry: Player) { 59 | val displayName: Component = entry.displayName 60 | target.tabList.getEntry(entry.uniqueId).ifPresentOrElse( 61 | { it.setDisplayName(displayName) }, 62 | { 63 | target.tabList.addEntry( 64 | TabListEntry.builder() 65 | .tabList(target.tabList) 66 | .profile(entry.gameProfile) 67 | .displayName(displayName) 68 | .latency(entry.ping.toInt()) 69 | .build() 70 | ) 71 | } 72 | ) 73 | } 74 | 75 | private inline val Player.displayName: Component 76 | get() { 77 | val serverInfo: ServerInfo = currentServer.get().serverInfo 78 | return miniMessage(config.format) { 79 | placeholders { 80 | unparsed("server_name", serverInfo.name) 81 | parsed("server_nickname", serverInfo.nickname) 82 | unparsed("player_name", username) 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve. 3 | title: "[Bug] " 4 | labels: 5 | - "type: bug" 6 | - "resolution: unresolved" 7 | - "status: awaiting response" 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: "**Note: Please fill this report truthfully, otherwise your issue may be closed, locked or deleted directly.**" 12 | 13 | - type: checkboxes 14 | id: before_reporting 15 | attributes: 16 | label: Before reporting 17 | options: 18 | - label: I have known and agreed that I would fill this report truthfully, or my issue may be closed, locked or deleted unconditionally. 19 | required: true 20 | - label: I have searched for existing issues (including `Open` and `Closed`). 21 | required: true 22 | - label: I am using the latest CI build of AdvancedVelocityManager. 23 | required: false 24 | 25 | - type: input 26 | id: plugin_version 27 | attributes: 28 | label: Plugin version 29 | description: The version of AdvancedVelocityManager you are using. 30 | validations: 31 | required: true 32 | 33 | - type: input 34 | id: velocity_version 35 | attributes: 36 | label: Velocity version 37 | description: The version of Velocity you are using (execute command `/velocity info` to retrieve). 38 | placeholder: Velocity 3.3.0-SNAPSHOT (git-00ed2284-b415) 39 | validations: 40 | required: true 41 | 42 | 43 | - type: textarea 44 | id: other_plugins 45 | attributes: 46 | label: Other installed plugins and its versions (Optional) 47 | description: The plugins installed on your Velocity server 48 | placeholder: | 49 | Floodgate 2.2.3-SNAPSHOT (b107-c4a4487) 50 | ViaVersion 5.0.1 51 | validations: 52 | required: false 53 | 54 | - type: textarea 55 | id: expected_behavior 56 | attributes: 57 | label: Expected behavior 58 | description: A clear and concise description of what should happen. 59 | validations: 60 | required: true 61 | 62 | - type: textarea 63 | id: actual_behavior 64 | attributes: 65 | label: Actual behavior 66 | description: A clear and concise description of what happens actually. 67 | validations: 68 | required: true 69 | 70 | - type: textarea 71 | id: steps_to_reproduce 72 | attributes: 73 | label: Steps to reproduce 74 | description: Steps to reproduce the behavior 75 | placeholder: | 76 | 1. Go to '...' 77 | 2. Click on '....' 78 | 3. Scroll down to '....' 79 | 4. Got error 80 | validations: 81 | required: true 82 | 83 | - type: textarea 84 | id: logs 85 | attributes: 86 | label: Logs 87 | description: Paste or upload logs here if possible. 88 | validations: 89 | required: false 90 | 91 | - type: textarea 92 | id: additional_information 93 | attributes: 94 | label: Additional information (Optional) 95 | description: | 96 | Add any other information about the problem here. 97 | Example: Plugin configuration, Affected Minecraft version(s), Operating System (and its version) and etc. 98 | validations: 99 | required: false 100 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/reconnect/ReconnectHandler.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.reconnect 2 | 3 | import com.velocitypowered.api.event.EventTask 4 | import com.velocitypowered.api.event.Subscribe 5 | import com.velocitypowered.api.event.player.KickedFromServerEvent 6 | import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction 7 | import com.velocitypowered.proxy.protocol.StateRegistry 8 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.eventManager 9 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 10 | import work.msdnicrosoft.avm.config.ConfigManager 11 | import work.msdnicrosoft.avm.packet.s2c.PlayerAbilitiesPacket 12 | import work.msdnicrosoft.avm.util.component.ComponentSerializer 13 | import work.msdnicrosoft.avm.util.component.orEmpty 14 | import work.msdnicrosoft.avm.util.packet.MinecraftVersion 15 | import work.msdnicrosoft.avm.util.packet.Packet 16 | 17 | object ReconnectHandler { 18 | private inline val config get() = ConfigManager.config.reconnect 19 | 20 | private inline val regex: Regex get() = Regex(config.pattern) 21 | 22 | // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol_version_numbers 23 | // https://minecraft.wiki/w/Java_Edition_protocol/Packets#Player_Abilities_(clientbound) 24 | @Suppress("MagicNumber") 25 | private val packet: Packet = Packet.of(PlayerAbilitiesPacket::class) 26 | .direction(Direction.CLIENTBOUND) 27 | .stateRegistry(StateRegistry.PLAY) 28 | .packetSupplier(::PlayerAbilitiesPacket) 29 | .mapping(0x39, MinecraftVersion.MINECRAFT_1_7_2, true) 30 | .mapping(0x2B, MinecraftVersion.MINECRAFT_1_9, true) 31 | .mapping(0x2C, MinecraftVersion.MINECRAFT_1_12_1, true) 32 | .mapping(0x2E, MinecraftVersion.MINECRAFT_1_13, true) 33 | .mapping(0x31, MinecraftVersion.MINECRAFT_1_14, true) 34 | .mapping(0x32, MinecraftVersion.MINECRAFT_1_15, true) 35 | .mapping(0x31, MinecraftVersion.MINECRAFT_1_16, true) 36 | .mapping(0x30, MinecraftVersion.MINECRAFT_1_16_2, true) 37 | .mapping(0x32, MinecraftVersion.MINECRAFT_1_17, true) 38 | .mapping(0x2F, MinecraftVersion.MINECRAFT_1_19, true) 39 | .mapping(0x31, MinecraftVersion.MINECRAFT_1_19_1, true) 40 | .mapping(0x30, MinecraftVersion.MINECRAFT_1_19_3, true) 41 | .mapping(0x34, MinecraftVersion.MINECRAFT_1_19_4, true) 42 | .mapping(0x36, MinecraftVersion.MINECRAFT_1_20_2, true) 43 | .mapping(0x38, MinecraftVersion.MINECRAFT_1_20_5, true) 44 | .mapping(0x3A, MinecraftVersion.MINECRAFT_1_21_2, true) 45 | .mapping(0x39, MinecraftVersion.MINECRAFT_1_21_5, true) 46 | 47 | fun init() { 48 | this.packet.register() 49 | eventManager.register(plugin, this) 50 | } 51 | 52 | fun disable() { 53 | this.packet.unregister() 54 | eventManager.unregisterListener(plugin, this) 55 | } 56 | 57 | @Subscribe 58 | fun onKickedFromServer(event: KickedFromServerEvent): EventTask? { 59 | if (event.kickedDuringServerConnect()) return null 60 | 61 | val reason: String = ComponentSerializer.BASIC_PLAIN_TEXT.serialize(event.serverKickReason.orEmpty()) 62 | 63 | if (!this.regex.matches(reason)) return null 64 | 65 | return EventTask.withContinuation { continuation -> 66 | Reconnection(event, continuation).reconnect() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/utility/ImportCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.utility 2 | 3 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 4 | import work.msdnicrosoft.avm.config.ConfigManager 5 | import work.msdnicrosoft.avm.module.command.session.CommandSessionManager 6 | import work.msdnicrosoft.avm.module.imports.PluginName 7 | import work.msdnicrosoft.avm.util.command.builder.* 8 | import work.msdnicrosoft.avm.util.command.context.name 9 | import work.msdnicrosoft.avm.util.command.data.server.Server 10 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 11 | import kotlin.time.Duration 12 | import kotlin.time.measureTimedValue 13 | 14 | object ImportCommand { 15 | val command = literalCommand("import") { 16 | requires { hasPermission("avm.command.import") } 17 | wordArgument("pluginName") { 18 | suggests { builder -> 19 | PluginName.PLUGINS.forEach(builder::suggest) 20 | builder.buildFuture() 21 | } 22 | wordArgument("defaultServer") { 23 | suggests { builder -> 24 | server.allServers.forEach { builder.suggest(it.serverInfo.name) } 25 | ConfigManager.config.whitelist.serverGroups.keys.forEach(builder::suggest) 26 | builder.buildFuture() 27 | } 28 | executes { 29 | val pluginName: String by this 30 | val defaultServer: Server by this 31 | 32 | val sessionId: String = CommandSessionManager.generateSessionId( 33 | context.source.name, 34 | System.currentTimeMillis(), 35 | context.arguments.values.joinToString(" ") 36 | ) 37 | 38 | CommandSessionManager.add(sessionId) { 39 | val (success: Boolean, elapsed: Duration) = measureTimedValue { 40 | PluginName.from(pluginName).import(this, defaultServer.name) 41 | } 42 | 43 | if (success) { 44 | sendTranslatable("avm.command.avm.import.success") { 45 | args { 46 | string("plugin_name", pluginName) 47 | string("elapsed", elapsed.toString()) 48 | } 49 | } 50 | } else { 51 | sendTranslatable("avm.command.avm.import.failed") { 52 | args { string("plugin_name", pluginName) } 53 | } 54 | } 55 | } 56 | sendTranslatable("avm.command.avm.import.need_confirm.1.text") 57 | sendMessage { 58 | translatable("avm.command.avm.import.need_confirm.2.text") { 59 | args { string("command", "/avm confirm $sessionId") } 60 | } styled { 61 | hoverText { tr("avm.command.avm.import.need_confirm.2.hover") } 62 | click { runCommand("/avm confirm $sessionId") } 63 | } 64 | } 65 | Command.SINGLE_SUCCESS 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/builder/text/ComponentBuilder.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component.builder.text 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.kyori.adventure.text.ComponentLike 5 | import net.kyori.adventure.text.JoinConfiguration 6 | import net.kyori.adventure.text.format.Style 7 | import work.msdnicrosoft.avm.annotations.dsl.ComponentDSL 8 | import work.msdnicrosoft.avm.util.component.ComponentSerializer 9 | import work.msdnicrosoft.avm.util.component.builder.minimessage.MiniMessageBuilder 10 | import work.msdnicrosoft.avm.util.component.builder.minimessage.miniMessage 11 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.TranslatableBuilder 12 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 13 | import work.msdnicrosoft.avm.util.component.builder.style.StyleBuilder 14 | import work.msdnicrosoft.avm.util.component.builder.style.style 15 | 16 | @Suppress("unused") 17 | @ComponentDSL 18 | class ComponentBuilder(private val joinConfiguration: JoinConfiguration) { 19 | private val components: MutableList = mutableListOf() 20 | 21 | private var replacements: TextReplacementsBuilder? = null 22 | 23 | fun empty(): ComponentBuilder = this.apply { this.components.add(Component.empty()) } 24 | 25 | fun newline(): ComponentBuilder = this.apply { this.components.add(Component.newline()) } 26 | 27 | fun space(): ComponentBuilder = this.apply { this.components.add(Component.space()) } 28 | 29 | fun keybind(keybind: String): ComponentBuilder = this.apply { this.components.add(Component.keybind(keybind)) } 30 | 31 | fun text(text: String): ComponentBuilder = this.apply { this.components.add(Component.text(text)) } 32 | 33 | fun text(text: () -> String): ComponentBuilder = this.apply { this.components.add(Component.text(text())) } 34 | 35 | fun translatable(key: String, builder: TranslatableBuilder.() -> Unit = {}): ComponentBuilder = this.apply { 36 | this.components.add(tr(key, builder)) 37 | } 38 | 39 | fun componentLike(component: ComponentLike): ComponentBuilder = this.apply { 40 | this.components.add(component.asComponent()) 41 | } 42 | 43 | fun mini( 44 | text: String, 45 | provider: ComponentSerializer = ComponentSerializer.MINI_MESSAGE, 46 | builder: MiniMessageBuilder.() -> Unit = {} 47 | ): ComponentBuilder = this.apply { this.components.add(miniMessage(text, provider, builder)) } 48 | 49 | fun replacements(builder: TextReplacementsBuilder.() -> Unit): ComponentBuilder = this.apply { 50 | val parent: TextReplacementsBuilder? = this.replacements 51 | this.replacements = TextReplacementsBuilder(parent).apply(builder) 52 | } 53 | 54 | inline infix fun styled(builder: StyleBuilder.() -> Unit): ComponentBuilder = this.with(style(builder)) 55 | 56 | infix fun with(style: Style): ComponentBuilder = this.apply { 57 | val last: Component = this.components.last() 58 | this.components[this.components.lastIndex] = last.style(style) 59 | } 60 | 61 | fun build(): Component = Component.join( 62 | this.joinConfiguration, 63 | this.components.map { this.replacements?.replace(it) ?: it } 64 | ) 65 | } 66 | 67 | inline fun component( 68 | joinConfiguration: JoinConfiguration = JoinConfiguration.noSeparators(), 69 | builder: ComponentBuilder.() -> Unit 70 | ): Component = ComponentBuilder(joinConfiguration).apply(builder).build() 71 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/utility/SendCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.utility 2 | 3 | import com.velocitypowered.api.proxy.Player 4 | import com.velocitypowered.api.proxy.server.RegisteredServer 5 | import net.kyori.adventure.text.Component 6 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 7 | import work.msdnicrosoft.avm.util.command.builder.* 8 | import work.msdnicrosoft.avm.util.command.context.CommandContext 9 | import work.msdnicrosoft.avm.util.command.context.name 10 | import work.msdnicrosoft.avm.util.command.data.component.MiniMessage 11 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 12 | import work.msdnicrosoft.avm.util.server.nickname 13 | import work.msdnicrosoft.avm.util.server.sendToServer 14 | 15 | object SendCommand { 16 | val command = literalCommand("send") { 17 | requires { hasPermission("avm.command.send") } 18 | wordArgument("player") { 19 | suggests { builder -> 20 | server.allPlayers.forEach { builder.suggest(it.username) } 21 | builder.buildFuture() 22 | } 23 | wordArgument("server") { 24 | suggests { builder -> 25 | server.allServers.forEach { builder.suggest(it.serverInfo.name) } 26 | builder.buildFuture() 27 | } 28 | executes { 29 | val server: RegisteredServer by this 30 | val player: Player by this 31 | sendPlayer( 32 | player, 33 | server, 34 | tr("avm.command.avm.send.target") { 35 | args { 36 | string("executor", context.source.name) 37 | string("server", server.serverInfo.nickname) 38 | } 39 | } 40 | ) 41 | Command.SINGLE_SUCCESS 42 | } 43 | stringArgument("reason") { 44 | executes { 45 | val server: RegisteredServer by this 46 | val player: Player by this 47 | val reason: MiniMessage by this 48 | sendPlayer(player, server, reason.component) 49 | Command.SINGLE_SUCCESS 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | private fun CommandContext.sendPlayer(player: Player, registeredServer: RegisteredServer, reason: Component) { 57 | val serverNickname: String = registeredServer.serverInfo.nickname 58 | 59 | player.sendToServer(registeredServer).thenAcceptAsync { success: Boolean -> 60 | if (success) { 61 | this.sendTranslatable("avm.command.avm.send.executor.success") { 62 | args { 63 | string("player", player.name) 64 | string("server", serverNickname) 65 | } 66 | } 67 | player.sendMessage(reason) 68 | } else { 69 | this.sendTranslatable("avm.command.avm.send.executor.failed") { 70 | args { 71 | string("player", player.name) 72 | string("server", serverNickname) 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/patch/transformers/KeyedChatHandlerTransformer.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Portions of this code are modified from cancellable-chat and are licensed under the MIT License (MIT). 3 | * 4 | * https://github.com/ZhuRuoLing/cancellable-chat/blob/977f1dfef71d783b0a824e80ab36ce25d30f2e65 5 | * /src/main/java/icu/takeneko/cancellablechat/ClassTransformerImpl.java 6 | * 7 | * Copyright (c) 2024 竹若泠 8 | * 9 | * MIT License 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy 12 | * of this software and associated documentation files (the "Software"), to deal 13 | * in the Software without restriction, including without limitation the rights 14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the Software is 16 | * furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in all 19 | * copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | * SOFTWARE. 28 | */ 29 | 30 | package work.msdnicrosoft.avm.patch.transformers 31 | 32 | import com.highcapable.kavaref.extension.classOf 33 | import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedChatHandler 34 | import org.objectweb.asm.ClassReader 35 | import org.objectweb.asm.ClassWriter 36 | import org.objectweb.asm.Opcodes 37 | import org.objectweb.asm.tree.ClassNode 38 | import org.objectweb.asm.tree.InsnNode 39 | import org.objectweb.asm.tree.MethodNode 40 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 41 | import java.security.ProtectionDomain 42 | 43 | object KeyedChatHandlerTransformer : ClassTransformer { 44 | private const val TARGET_CLASS_NAME = "com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler" 45 | private const val TARGET_METHOD_NAME = "invalidCancel" 46 | private const val TARGET_METHOD_DESC = 47 | "(Lorg/apache/logging/log4j/Logger;Lcom/velocitypowered/proxy/connection/client/ConnectedPlayer;)V" 48 | 49 | override val targetClass: Class = classOf() 50 | 51 | override fun shouldTransform(): Boolean = server.pluginManager.getPlugin("signedvelocity").isEmpty 52 | 53 | override fun transform( 54 | loader: ClassLoader?, 55 | className: String, 56 | classBeingRedefined: Class<*>?, 57 | protectionDomain: ProtectionDomain, 58 | classfileBuffer: ByteArray 59 | ): ByteArray? { 60 | if (className != this.TARGET_CLASS_NAME) return null 61 | 62 | val node: ClassNode = ClassNode().apply { 63 | ClassReader(classfileBuffer).accept(this@apply, 0) 64 | } 65 | 66 | node.methods.find { method: MethodNode -> 67 | method.name == this.TARGET_METHOD_NAME && method.desc == this.TARGET_METHOD_DESC 68 | }?.instructions?.iterator()?.add(InsnNode(Opcodes.RETURN)) 69 | 70 | return ClassWriter(ClassWriter.COMPUTE_MAXS or ClassWriter.COMPUTE_FRAMES) 71 | .apply { node.accept(this) } 72 | .toByteArray() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/chatbridge/ChatBridge.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.chatbridge 2 | 3 | import com.velocitypowered.api.event.PostOrder 4 | import com.velocitypowered.api.event.Subscribe 5 | import com.velocitypowered.api.event.command.CommandExecuteEvent 6 | import com.velocitypowered.api.event.player.PlayerChatEvent 7 | import net.kyori.adventure.text.Component 8 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 9 | import work.msdnicrosoft.avm.command.chatbridge.MsgCommand 10 | import work.msdnicrosoft.avm.config.ConfigManager 11 | import work.msdnicrosoft.avm.module.Logging 12 | 13 | object ChatBridge { 14 | private inline val config get() = ConfigManager.config.chatBridge 15 | 16 | /** 17 | * The current passthrough mode for chat messages. 18 | * Defaults to [PassthroughMode.ALL]. 19 | */ 20 | var mode: PassthroughMode = PassthroughMode.ALL 21 | 22 | fun init() { 23 | plugin.server.eventManager.register(plugin, this) 24 | } 25 | 26 | fun disable() { 27 | plugin.server.eventManager.unregisterListener(plugin, this) 28 | } 29 | 30 | @Suppress("Deprecation") 31 | @Subscribe(order = PostOrder.FIRST) 32 | fun onPlayerChatChat(event: PlayerChatEvent) { 33 | if (!config.enabled) return 34 | 35 | val message: Component = ChatMessage(event.player, event.message).toComponent() 36 | val serverName: String = event.player.currentServer.get().serverInfo.name 37 | 38 | when (this.mode) { 39 | PassthroughMode.ALL -> this.sendMessage(message, serverName) 40 | PassthroughMode.NONE -> { 41 | event.result = PlayerChatEvent.ChatResult.denied() 42 | this.sendMessage(message) 43 | } 44 | 45 | PassthroughMode.PATTERN -> { 46 | val playerMessage = event.message 47 | val patterns = config.chatPassthrough.pattern 48 | val matched = patterns.contains.any { it in playerMessage } || 49 | patterns.startswith.any { playerMessage.startsWith(it) } || 50 | patterns.endswith.any { playerMessage.endsWith(it) } 51 | if (matched) { 52 | this.sendMessage(message, serverName) 53 | } else { 54 | event.result = PlayerChatEvent.ChatResult.denied() 55 | this.sendMessage(message) 56 | } 57 | } 58 | } 59 | 60 | if (config.logging) { 61 | Logging.log("[$serverName]<${event.player.username}> ${event.message}") 62 | } 63 | } 64 | 65 | @Subscribe 66 | fun onCommandExecute(event: CommandExecuteEvent) { 67 | val eventCommand: String = event.command 68 | .split(" ") 69 | .first() 70 | .replace("/", "") 71 | 72 | // Check if the executed command is related to private chat 73 | val isPrivateChat: Boolean = MsgCommand.aliases.any { eventCommand.startsWith(it) } 74 | if (!isPrivateChat) return 75 | 76 | if (!config.takeOverPrivateChat) { 77 | event.result = CommandExecuteEvent.CommandResult.forwardToServer() 78 | } 79 | } 80 | 81 | private fun sendMessage(message: Component, vararg ignoredServer: String) { 82 | plugin.server.allServers.parallelStream() 83 | .filter { server -> server.serverInfo.name !in ignoredServer } 84 | .forEach { server -> 85 | server.playersConnected.forEach { player -> 86 | player.sendMessage(message) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/utility/KickAllCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.utility 2 | 3 | import com.velocitypowered.api.proxy.Player 4 | import com.velocitypowered.api.proxy.server.RegisteredServer 5 | import net.kyori.adventure.text.Component 6 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 7 | import work.msdnicrosoft.avm.util.command.builder.* 8 | import work.msdnicrosoft.avm.util.command.context.CommandContext 9 | import work.msdnicrosoft.avm.util.command.context.name 10 | import work.msdnicrosoft.avm.util.command.data.component.MiniMessage 11 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 12 | import work.msdnicrosoft.avm.util.component.builder.style.styled 13 | import work.msdnicrosoft.avm.util.server.task 14 | 15 | object KickAllCommand { 16 | val command = literalCommand("kickall") { 17 | requires { hasPermission("avm.command.kickall") } 18 | executes { 19 | server.allPlayers 20 | .filterNot { player -> player.hasPermission("avm.kickall.bypass") } 21 | .forEach { player -> 22 | player.disconnect( 23 | tr("avm.command.avm.kick.target") { 24 | args { string("executor", context.source.name) } 25 | } 26 | ) 27 | } 28 | Command.SINGLE_SUCCESS 29 | } 30 | wordArgument("server") { 31 | suggests { builder -> 32 | server.allServers.forEach { builder.suggest(it.serverInfo.name) } 33 | builder.buildFuture() 34 | } 35 | executes { 36 | val server: RegisteredServer by this 37 | kickAllPlayers( 38 | server, 39 | tr("avm.command.avm.kick.target") { 40 | args { string("executor", context.source.name) } 41 | } 42 | ) 43 | Command.SINGLE_SUCCESS 44 | } 45 | stringArgument("reason") { 46 | executes { 47 | val server: RegisteredServer by this 48 | val reason: MiniMessage by this 49 | kickAllPlayers(server, reason.component) 50 | Command.SINGLE_SUCCESS 51 | } 52 | } 53 | } 54 | } 55 | 56 | private fun CommandContext.kickAllPlayers(registeredServer: RegisteredServer, reason: Component) { 57 | if (registeredServer.playersConnected.isEmpty()) { 58 | sendTranslatable("avm.general.empty.server") 59 | return 60 | } 61 | 62 | task { 63 | val allPlayers: Collection = registeredServer.playersConnected 64 | val toKick: List = allPlayers.filterNot { player -> 65 | player.hasPermission("avm.kickall.bypass") 66 | } 67 | 68 | toKick.forEach { player -> 69 | player.disconnect(reason) 70 | } 71 | sendTranslatable("avm.command.avm.kickall.executor.text") { 72 | args { 73 | numeric("player_total", toKick.size) 74 | component( 75 | "bypass", 76 | tr("avm.command.avm.kickall.executor.bypass.text") { 77 | args { numeric("player_bypass", allPlayers.size - toKick.size) } 78 | } styled { hoverText { tr("avm.command.avm.kickall.executor.bypass.hover") } } 79 | ) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/config/data/Broadcast.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MaxLineLength") 2 | 3 | package work.msdnicrosoft.avm.config.data 4 | 5 | import com.charleskorn.kaml.YamlComment 6 | import kotlinx.serialization.SerialName 7 | import kotlinx.serialization.Serializable 8 | 9 | @Serializable 10 | data class Broadcast( 11 | @YamlComment("When a player joins the server, plugin will send broadcast") 12 | @SerialName("join") 13 | val join: Join = Join(), 14 | 15 | @YamlComment("When a player leaves the server, plugin will send broadcast") 16 | @SerialName("leave") 17 | val leave: Leave = Leave(), 18 | 19 | @YamlComment("When a player switch from a server to another server, plugin will send broadcast") 20 | @SerialName("switch") 21 | val switch: Switch = Switch(), 22 | ) { 23 | @Serializable 24 | data class Join( 25 | @YamlComment("Whether to enable join broadcast") 26 | var enabled: Boolean = true, 27 | 28 | @YamlComment("Whether to enable logging join broadcast to file") 29 | val logging: Boolean = false, 30 | 31 | @YamlComment( 32 | "The join broadcast message", 33 | "", 34 | "Default: [+] joined server ", 35 | "", 36 | "Available placeholders:", 37 | " - Username of a player who joined the server", 38 | " - Server name which the player joined in", 39 | " - Server nickname which the player joined in" 40 | ) 41 | val message: String = "[+] joined server " 42 | ) 43 | 44 | @Serializable 45 | data class Leave( 46 | @YamlComment("Whether to enable leave broadcast") 47 | var enabled: Boolean = true, 48 | 49 | @YamlComment("Whether to enable logging leave broadcast to file") 50 | val logging: Boolean = false, 51 | 52 | @YamlComment( 53 | "The leave broadcast message", 54 | "", 55 | "Default: [-] left the server", 56 | "", 57 | "Available placeholders:", 58 | " - Username of a player who left the server" 59 | ) 60 | val message: String = "[-] left the server" 61 | ) 62 | 63 | @Serializable 64 | data class Switch( 65 | @YamlComment("Whether to enable switch broadcast") 66 | var enabled: Boolean = true, 67 | 68 | @YamlComment("Whether to enable logging switch broadcast to file") 69 | val logging: Boolean = false, 70 | 71 | @YamlComment( 72 | "The switch broadcast message", 73 | "", 74 | "Default: [] : ", 75 | "", 76 | "Available placeholders:", 77 | " - Username of a player who joined the server", 78 | " - Server name which the player switched from", 79 | " - Server nickname which the player switched from", 80 | " - Server name which the player switched to", 81 | " - Server name which the player switched to" 82 | ) 83 | val message: String = 84 | "[] : " 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/utility/SendAllCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.utility 2 | 3 | import com.velocitypowered.api.proxy.Player 4 | import com.velocitypowered.api.proxy.server.RegisteredServer 5 | import net.kyori.adventure.text.Component 6 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 7 | import work.msdnicrosoft.avm.util.command.builder.* 8 | import work.msdnicrosoft.avm.util.command.context.CommandContext 9 | import work.msdnicrosoft.avm.util.command.context.name 10 | import work.msdnicrosoft.avm.util.command.data.component.MiniMessage 11 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 12 | import work.msdnicrosoft.avm.util.component.builder.style.styled 13 | import work.msdnicrosoft.avm.util.server.nickname 14 | import work.msdnicrosoft.avm.util.server.sendToServer 15 | import work.msdnicrosoft.avm.util.server.task 16 | 17 | object SendAllCommand { 18 | val command = literalCommand("sendall") { 19 | requires { hasPermission("avm.command.sendall") } 20 | wordArgument("server") { 21 | suggests { builder -> 22 | server.allServers.forEach { builder.suggest(it.serverInfo.name) } 23 | builder.buildFuture() 24 | } 25 | executes { 26 | val server: RegisteredServer by this 27 | sendAllPlayers( 28 | server, 29 | tr("avm.command.avm.send.target") { 30 | args { 31 | string("executor", context.source.name) 32 | string("server", server.serverInfo.nickname) 33 | } 34 | } 35 | ) 36 | Command.SINGLE_SUCCESS 37 | } 38 | stringArgument("reason") { 39 | executes { 40 | val server: RegisteredServer by this 41 | val reason: MiniMessage by this 42 | sendAllPlayers(server, reason.component) 43 | Command.SINGLE_SUCCESS 44 | } 45 | } 46 | } 47 | } 48 | 49 | private fun CommandContext.sendAllPlayers(registeredServer: RegisteredServer, reason: Component) { 50 | if (registeredServer.playersConnected.isEmpty()) { 51 | this.sendTranslatable("avm.general.empty.server") 52 | return 53 | } 54 | 55 | task { 56 | val allPlayers: List = server.allPlayers.filterNot { player -> 57 | player.currentServer.get() == registeredServer 58 | } 59 | val toSend: List = allPlayers.filterNot { player -> 60 | player.hasPermission("avm.sendall.bypass") 61 | } 62 | 63 | toSend.forEach { player -> 64 | player.sendToServer(registeredServer).thenAcceptAsync { success: Boolean -> 65 | if (success) player.sendMessage(reason) 66 | } 67 | } 68 | 69 | this.sendTranslatable("avm.command.avm.sendall.executor.text") { 70 | args { 71 | numeric("player_total", toSend.size) 72 | string("server", registeredServer.serverInfo.nickname) 73 | component( 74 | "bypass", 75 | tr("avm.command.avm.sendall.executor.bypass.text") { 76 | args { numeric("player_bypass", allPlayers.size - toSend.size) } 77 | } styled { hoverText { tr("avm.command.avm.sendall.executor.bypass.hover") } } 78 | ) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/net/http/HttpStatus.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.net.http 2 | 3 | @Suppress("MagicNumber") 4 | enum class HttpStatus(val value: Int, val description: String) { 5 | CONTINUE(100, "Continue"), 6 | SWITCHING_PROTOCOLS(101, "Switching Protocols"), 7 | PROCESSING(102, "Processing"), 8 | EARLY_HINTS(103, "Early Hints"), 9 | 10 | OK(200, "OK"), 11 | CREATED(201, "Created"), 12 | ACCEPTED(202, "Accepted"), 13 | NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"), 14 | NO_CONTENT(204, "No Content"), 15 | RESET_CONTENT(205, "Reset Content"), 16 | PARTIAL_CONTENT(206, "Partial Content"), 17 | MULTI_STATUS(207, "Multi-Status"), 18 | ALREADY_REPORTED(208, "Already Reported"), 19 | IM_USED(226, "IM Used"), 20 | 21 | MULTIPLE_CHOICES(300, "Multiple Choices"), 22 | MOVED_PERMANENTLY(301, "Moved Permanently"), 23 | FOUND(302, "Found"), 24 | SEE_OTHER(303, "See Other"), 25 | NOT_MODIFIED(304, "Not Modified"), 26 | USE_PROXY(305, "Use Proxy"), 27 | TEMPORARY_REDIRECT(307, "Temporary Redirect"), 28 | PERMANENT_REDIRECT(308, "Permanent Redirect"), 29 | 30 | BAD_REQUEST(400, "Bad Request"), 31 | UNAUTHORIZED(401, "Unauthorized"), 32 | PAYMENT_REQUIRED(402, "Payment Required"), 33 | FORBIDDEN(403, "Forbidden"), 34 | NOT_FOUND(404, "Not Found"), 35 | METHOD_NOT_ALLOWED(405, "Method Not Allowed"), 36 | NOT_ACCEPTABLE(406, "Not Acceptable"), 37 | PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"), 38 | REQUEST_TIMEOUT(408, "Request Timeout"), 39 | CONFLICT(409, "Conflict"), 40 | GONE(410, "Gone"), 41 | LENGTH_REQUIRED(411, "Length Required"), 42 | PRECONDITION_FAILED(412, "Precondition Failed"), 43 | CONTENT_TOO_LARGE(413, "Content Too Large"), 44 | REQUEST_ENTITY_TOO_LARGE(413, "Content Too Large"), 45 | URI_TOO_LONG(414, "URI Too Long"), 46 | REQUEST_URI_TOO_LONG(414, "URI Too Long"), 47 | UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), 48 | RANGE_NOT_SATISFIABLE(416, "Range Not Satisfiable"), 49 | REQUESTED_RANGE_NOT_SATISFIABLE(416, "Range Not Satisfiable"), 50 | EXPECTATION_FAILED(417, "Expectation Failed"), 51 | IM_A_TEAPOT(418, "I'm a Teapot"), 52 | MISDIRECTED_REQUEST(421, "Misdirected Request"), 53 | UNPROCESSABLE_CONTENT(422, "Unprocessable Content"), 54 | UNPROCESSABLE_ENTITY(422, "Unprocessable Content"), 55 | LOCKED(423, "Locked"), 56 | FAILED_DEPENDENCY(424, "Failed Dependency"), 57 | TOO_EARLY(425, "Too Early"), 58 | UPGRADE_REQUIRED(426, "Upgrade Required"), 59 | PRECONDITION_REQUIRED(428, "Precondition Required"), 60 | TOO_MANY_REQUESTS(429, "Too Many Requests"), 61 | REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"), 62 | UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"), 63 | 64 | INTERNAL_SERVER_ERROR(500, "Internal Server Error"), 65 | NOT_IMPLEMENTED(501, "Not Implemented"), 66 | BAD_GATEWAY(502, "Bad Gateway"), 67 | SERVICE_UNAVAILABLE(503, "Service Unavailable"), 68 | GATEWAY_TIMEOUT(504, "Gateway Timeout"), 69 | HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version Not Supported"), 70 | VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates"), 71 | INSUFFICIENT_STORAGE(507, "Insufficient Storage"), 72 | LOOP_DETECTED(508, "Loop Detected"), 73 | NOT_EXTENDED(510, "Not Extended"), 74 | NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required"); 75 | 76 | fun isSuccess(): Boolean = this.value in 200..299 77 | 78 | companion object { 79 | private val statusCodesMap: Map = entries.associateBy { it.value } 80 | 81 | fun fromValue(value: Int): HttpStatus = statusCodesMap[value] ?: error("Unknown HTTP status code: $value") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/component/ComponentSerializer.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.component 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.kyori.adventure.text.flattener.ComponentFlattener 5 | import net.kyori.adventure.text.minimessage.MiniMessage 6 | import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver 7 | import net.kyori.adventure.text.minimessage.tag.standard.StandardTags 8 | import net.kyori.adventure.text.serializer.ComponentSerializer 9 | import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer 10 | import net.kyori.adventure.text.serializer.json.JSONComponentSerializer 11 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer 12 | import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer 13 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.logger 14 | 15 | @Suppress("unused") 16 | enum class ComponentSerializer { 17 | JSON { 18 | override val serializer: JSONComponentSerializer by lazy { JSONComponentSerializer.json() } 19 | }, 20 | GSON { 21 | override val serializer: GsonComponentSerializer by lazy { GsonComponentSerializer.gson() } 22 | }, 23 | LEGACY_SECTION { 24 | override val serializer: LegacyComponentSerializer by lazy { LegacyComponentSerializer.legacySection() } 25 | }, 26 | LEGACY_AMPERSAND { 27 | override val serializer: LegacyComponentSerializer by lazy { LegacyComponentSerializer.legacyAmpersand() } 28 | }, 29 | MINI_MESSAGE { 30 | override val serializer: MiniMessage by lazy { MiniMessage.miniMessage() } 31 | 32 | override fun deserialize(text: String, vararg tagResolver: TagResolver): Component = 33 | this.serializer.deserialize(text, *tagResolver) 34 | }, 35 | STYLE_ONLY_MINI_MESSAGE { 36 | override val serializer: MiniMessage by lazy { 37 | MiniMessage.builder() 38 | .tags( 39 | TagResolver.builder() 40 | .resolver(StandardTags.font()) 41 | .resolver(StandardTags.color()) 42 | .resolver(StandardTags.decorations()) 43 | .resolver(StandardTags.gradient()) 44 | .resolver(StandardTags.rainbow()) 45 | .resolver(StandardTags.reset()) 46 | .resolver(StandardTags.shadowColor()) 47 | .build() 48 | ).build() 49 | } 50 | 51 | override fun deserialize(text: String, vararg tagResolver: TagResolver): Component = 52 | this.serializer.deserialize(text, *tagResolver) 53 | }, 54 | PLAIN_TEXT { 55 | override val serializer: PlainTextComponentSerializer by lazy { PlainTextComponentSerializer.plainText() } 56 | }, 57 | BASIC_PLAIN_TEXT { 58 | override val serializer: PlainTextComponentSerializer by lazy { 59 | PlainTextComponentSerializer.builder() 60 | .flattener(ComponentFlattener.basic()) 61 | .build() 62 | } 63 | }; 64 | 65 | abstract val serializer: ComponentSerializer 66 | 67 | open fun deserialize(text: String, vararg tagResolver: TagResolver): Component { 68 | logger.warn("The MiniMessage tag resolver is not supported in this serialization type.") 69 | return this.serializer.deserialize(text) 70 | } 71 | 72 | open fun deserialize(text: String): Component = this.serializer.deserialize(text) 73 | open fun deserializeOrNull(text: String?): Component? = this.serializer.deserializeOrNull(text) 74 | 75 | open fun serialize(component: Component): String = this.serializer.serialize(component) 76 | open fun serializeOrNull(component: Component?): String? = this.serializer.serializeOrNull(component) 77 | } 78 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/chatbridge/ChatMessage.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.chatbridge 2 | 3 | import com.velocitypowered.api.proxy.Player 4 | import com.velocitypowered.api.proxy.ServerConnection 5 | import net.kyori.adventure.text.Component 6 | import net.kyori.adventure.text.JoinConfiguration 7 | import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder 8 | import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver 9 | import work.msdnicrosoft.avm.config.ConfigManager 10 | import work.msdnicrosoft.avm.util.DateTimeUtil.getDateTime 11 | import work.msdnicrosoft.avm.util.component.ComponentSerializer 12 | import work.msdnicrosoft.avm.util.component.builder.minimessage.miniMessage 13 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.placeholders 14 | import work.msdnicrosoft.avm.util.component.builder.style.styled 15 | import work.msdnicrosoft.avm.util.server.ProxyServerUtil.TIMEOUT_PING_RESULT 16 | import work.msdnicrosoft.avm.util.server.nickname 17 | import java.util.concurrent.TimeUnit 18 | import java.util.concurrent.TimeoutException 19 | 20 | class ChatMessage(private val player: Player, private val message: String) { 21 | private val serverConnection: ServerConnection = player.currentServer.get() 22 | private val serverVersion: String by lazy { 23 | try { 24 | player.currentServer.get().server.ping().get(20, TimeUnit.MILLISECONDS) 25 | } catch (_: TimeoutException) { 26 | TIMEOUT_PING_RESULT 27 | }.version.name 28 | } 29 | 30 | private val basicPlaceHolders: List = placeholders { 31 | unparsed("player_name", player.username) 32 | unparsed("player_uuid", player.uniqueId.toString()) 33 | numeric("player_ping", player.ping) 34 | unparsed("server_name", serverConnection.serverInfo.name) 35 | unparsed("server_nickname", serverConnection.serverInfo.nickname) 36 | numeric("server_online_players", serverConnection.server.playersConnected.size) 37 | unparsed("player_message_sent_time", getDateTime()) 38 | if (config.allowFormatCode) { 39 | parsed("player_message", message) 40 | } else { 41 | unparsed("player_message", message) 42 | } 43 | } 44 | 45 | fun toComponent(): Component = Component.join( 46 | JoinConfiguration.noSeparators(), 47 | config.publicChatFormat.map { format -> 48 | val tagResolvers: List = buildList { 49 | addAll(basicPlaceHolders) 50 | if ("" in format.text) { 51 | add(Placeholder.unparsed("server_version", serverVersion)) 52 | } 53 | } 54 | miniMessage(format.text, provider = ComponentSerializer.STYLE_ONLY_MINI_MESSAGE) { 55 | placeholders { tagResolvers(tagResolvers) } 56 | } styled { 57 | hoverText { 58 | miniMessage(format.hover?.joinToString("\n").orEmpty()) { 59 | placeholders { tagResolvers(tagResolvers) } 60 | } 61 | } 62 | click(format.applyReplace { replacePlaceholders() }) 63 | } 64 | } 65 | ) 66 | 67 | private fun String.replacePlaceholders(): String = this 68 | .replace("", player.username) 69 | .replace("", player.uniqueId.toString()) 70 | .replace("", player.ping.toString()) 71 | .replace("", message) 72 | .replace("", serverConnection.serverInfo.name) 73 | .replace("", serverConnection.serverInfo.nickname) 74 | .replace("", serverConnection.server.playersConnected.size.toString()) 75 | .let { if ("" in this) it.replace("", serverVersion) else it } 76 | 77 | companion object { 78 | private inline val config get() = ConfigManager.config.chatBridge 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/context/CommandContextExtension.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.context 2 | 3 | import com.highcapable.kavaref.KavaRef.Companion.resolve 4 | import com.highcapable.kavaref.extension.classOf 5 | import com.mojang.brigadier.exceptions.CommandSyntaxException 6 | import com.mojang.brigadier.exceptions.SimpleCommandExceptionType 7 | import com.velocitypowered.api.command.VelocityBrigadierMessage 8 | import net.kyori.adventure.text.Component 9 | import net.kyori.adventure.text.JoinConfiguration 10 | import net.kyori.adventure.text.format.NamedTextColor 11 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 12 | import work.msdnicrosoft.avm.annotations.command.CommandNode 13 | import work.msdnicrosoft.avm.annotations.command.RootCommand 14 | import work.msdnicrosoft.avm.util.command.builder.Command 15 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 16 | import work.msdnicrosoft.avm.util.component.builder.style.styled 17 | import work.msdnicrosoft.avm.util.component.builder.text.component 18 | import work.msdnicrosoft.avm.util.reflect.getAnnotationIfPresent 19 | 20 | @Throws(CommandSyntaxException::class) 21 | fun throwCommandException(message: Component): Nothing = 22 | throw SimpleCommandExceptionType(VelocityBrigadierMessage.tooltip(message)).create() 23 | 24 | @Suppress("UnsafeCallOnNullableType", "SameReturnValue") 25 | fun CommandContext.buildHelp(commandRoot: Class<*>, checkPermission: Boolean = true): Int { 26 | val rootCommand: RootCommand = commandRoot.getAnnotationIfPresent() ?: return Command.SINGLE_SUCCESS 27 | val rootName: String = rootCommand.name 28 | 29 | sendTranslatable("avm.general.help.header.1.text") { 30 | args { 31 | component( 32 | "name", 33 | tr("avm.general.plugin.name") styled { 34 | hoverText { tr("avm.general.help.header.1.name.hover") } 35 | } 36 | ) 37 | string("version", plugin.self.version.get()) 38 | } 39 | } 40 | sendTranslatable("avm.general.help.header.2.text") { 41 | args { string("root_command", rootName) } 42 | } 43 | sendTranslatable("avm.general.help.header.subcommands") 44 | 45 | commandRoot.resolve() 46 | .field { 47 | annotations(CommandNode::class) 48 | }.map { resolver -> 49 | resolver.self.getAnnotation(classOf()) to resolver.get()!! 50 | }.forEach { (commandNode, command) -> 51 | if (checkPermission && !command.node.requirement.test(context.source)) return@forEach 52 | 53 | val arguments: String = commandNode.arguments.joinToString(" ") { arg -> 54 | when (arg.firstOrNull()) { 55 | '[' -> "$arg" 56 | '<' -> "$arg" 57 | else -> arg 58 | } 59 | } 60 | 61 | val description: Component = tr("avm.command.$rootName.${commandNode.name}.description") 62 | 63 | sendMessage(JoinConfiguration.spaces()) { 64 | text(" -") styled { color(NamedTextColor.DARK_GRAY) } 65 | mini("${commandNode.name} $arguments") styled { 66 | color(NamedTextColor.WHITE) 67 | hoverText { 68 | component(JoinConfiguration.spaces()) { 69 | text("$rootName ${commandNode.name}") styled { color(NamedTextColor.WHITE) } 70 | text("-") styled { color(NamedTextColor.DARK_GRAY) } 71 | componentLike(description) styled { color(NamedTextColor.GRAY) } 72 | } 73 | } 74 | click { suggestCommand("/$rootName ${commandNode.name} $arguments") } 75 | } 76 | } 77 | sendMessage { 78 | text(" ") 79 | componentLike(description) styled { color(NamedTextColor.GRAY) } 80 | } 81 | } 82 | return Command.SINGLE_SUCCESS 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/imports/importers/QuAnVelocityWhitelistImporter.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.imports.importers 2 | 3 | import com.moandjiezana.toml.Toml 4 | import kotlinx.serialization.Serializable 5 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.dataDirectory 6 | import work.msdnicrosoft.avm.config.ConfigManager 7 | import work.msdnicrosoft.avm.module.whitelist.WhitelistManager 8 | import work.msdnicrosoft.avm.util.command.context.CommandContext 9 | import work.msdnicrosoft.avm.util.data.UUIDSerializer 10 | import work.msdnicrosoft.avm.util.file.FileUtil.JSON 11 | import work.msdnicrosoft.avm.util.file.FileUtil.TOML 12 | import work.msdnicrosoft.avm.util.file.readTextWithBuffer 13 | import work.msdnicrosoft.avm.util.net.http.YggdrasilApiUtil 14 | import java.nio.file.Path 15 | import java.util.* 16 | import kotlin.io.path.div 17 | import kotlin.io.path.exists 18 | 19 | object QuAnVelocityWhitelistImporter : Importer { 20 | @Serializable 21 | private data class Player( 22 | @Serializable(with = UUIDSerializer::class) 23 | val uuid: UUID, 24 | val name: String 25 | ) 26 | 27 | private val PATH: Path = dataDirectory.parent / "VelocityWhitelist" 28 | private val CONFIG_PATH: Path = this.PATH / "config.toml" 29 | private val WHITELIST_PATH: Path = 30 | if (YggdrasilApiUtil.serverIsOnlineMode) { 31 | this.PATH / "whitelist.json" 32 | } else { 33 | this.PATH / "whitelist_offline.json" 34 | } 35 | 36 | override val displayName: String = "(qu-an) VelocityWhitelist" 37 | 38 | override fun import(context: CommandContext, defaultServer: String): Boolean { 39 | val configSuccess: Boolean = if (this.CONFIG_PATH.exists()) { 40 | importConfig(context) 41 | } else { 42 | context.sendTranslatable("avm.command.avm.import.config.not_exist") { 43 | args { string("plugin_name", displayName) } 44 | } 45 | true 46 | } 47 | 48 | val whitelistSuccess: Boolean = if (this.WHITELIST_PATH.exists()) { 49 | importWhitelist(defaultServer, context) 50 | } else { 51 | context.sendTranslatable("avm.command.avm.import.whitelist.not_exist") { 52 | args { string("plugin_name", displayName) } 53 | } 54 | true 55 | } 56 | 57 | return configSuccess && whitelistSuccess 58 | } 59 | 60 | private fun importConfig(context: CommandContext): Boolean = 61 | try { 62 | val config: Toml = TOML.read(this.CONFIG_PATH.readTextWithBuffer()) 63 | ConfigManager.config.run { 64 | whitelist.enabled = config.getBoolean("enable_whitelist") 65 | whitelist.queryApi.uuid = config.getString("uuid_api") 66 | whitelist.queryApi.profile = config.getString("profile_api") 67 | } 68 | ConfigManager.save() 69 | } catch (e: Exception) { 70 | context.sendTranslatable("avm.command.avm.import.config.failed") { 71 | args { 72 | string("plugin_name", displayName) 73 | string("reason", e.message.orEmpty()) 74 | } 75 | } 76 | false 77 | } 78 | 79 | private fun importWhitelist(defaultServer: String, context: CommandContext): Boolean = 80 | try { 81 | val whitelist: List = JSON.decodeFromString(this.WHITELIST_PATH.readTextWithBuffer()) 82 | val onlineMode: Boolean = YggdrasilApiUtil.serverIsOnlineMode 83 | 84 | whitelist.forEach { player -> 85 | WhitelistManager.add(player.name, player.uuid, defaultServer, onlineMode) 86 | } 87 | true 88 | } catch (e: Exception) { 89 | context.sendTranslatable("avm.command.avm.import.whitelist.failed") { 90 | args { 91 | string("plugin_name", displayName) 92 | string("reason", e.message.orEmpty()) 93 | } 94 | } 95 | false 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/command/context/ArgumentParser.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.command.context 2 | 3 | import com.velocitypowered.api.proxy.Player 4 | import com.velocitypowered.api.proxy.server.RegisteredServer 5 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 6 | import work.msdnicrosoft.avm.config.ConfigManager 7 | import work.msdnicrosoft.avm.util.command.data.PlayerByUUID 8 | import work.msdnicrosoft.avm.util.command.data.component.MiniMessage 9 | import work.msdnicrosoft.avm.util.command.data.server.Server 10 | import work.msdnicrosoft.avm.util.command.data.server.ServerGroup 11 | import work.msdnicrosoft.avm.util.component.builder.minimessage.miniMessage 12 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 13 | import work.msdnicrosoft.avm.util.string.isUuid 14 | import work.msdnicrosoft.avm.util.string.toUuid 15 | import java.util.Optional 16 | import java.util.UUID 17 | import kotlin.reflect.KClass 18 | 19 | fun interface ArgumentParser { 20 | fun parse(argument: String): T 21 | 22 | companion object { 23 | private val UUIDParser = ArgumentParser { argument -> 24 | if (!argument.isUuid()) { 25 | throwCommandException(tr("avm.general.invalid.uuid") { args { string("uuid", argument) } }) 26 | } 27 | argument.toUuid() 28 | } 29 | 30 | private val PlayerParser = ArgumentParser { argument -> 31 | val player: Optional = server.getPlayer(argument) 32 | if (player.isEmpty) { 33 | throwCommandException(tr("avm.general.not_found.player") { args { string("player", argument) } }) 34 | } 35 | player.get() 36 | } 37 | 38 | private val PlayerByUUIDParser = ArgumentParser { argument -> 39 | val uuid: UUID = UUIDParser.parse(argument) 40 | val player: Optional = server.getPlayer(uuid) 41 | if (player.isEmpty) { 42 | throwCommandException(tr("avm.general.not_found.player") { args { string("player", argument) } }) 43 | } 44 | PlayerByUUID(player.get()) 45 | } 46 | 47 | private val ServerParser = ArgumentParser { argument -> 48 | if (!ConfigManager.config.whitelist.isServerGroup(argument) && server.getServer(argument).isEmpty) { 49 | throwCommandException(tr("avm.general.not_found.server") { args { string("server", argument) } }) 50 | } 51 | Server(argument) 52 | } 53 | 54 | private val RegisteredServerParser = ArgumentParser { argument -> 55 | val server: Optional = server.getServer(argument) 56 | if (server.isEmpty) { 57 | throwCommandException(tr("avm.general.not_found.server") { args { string("server", argument) } }) 58 | } 59 | server.get() 60 | } 61 | 62 | private val ServerGroupParser = ArgumentParser { argument -> 63 | if (!ConfigManager.config.whitelist.isServerGroup(argument)) { 64 | throwCommandException( 65 | tr("avm.general.not_found.server_group") { args { string("server_group", argument) } } 66 | ) 67 | } 68 | ServerGroup(argument) 69 | } 70 | 71 | private val MiniMessageParser = ArgumentParser { argument -> 72 | MiniMessage(miniMessage(argument)) 73 | } 74 | 75 | val parsers: Map, ArgumentParser<*>> = mapOf( 76 | UUID::class to UUIDParser, 77 | Player::class to PlayerParser, 78 | PlayerByUUID::class to PlayerByUUIDParser, 79 | Server::class to ServerParser, 80 | RegisteredServer::class to RegisteredServerParser, 81 | ServerGroup::class to ServerGroupParser, 82 | MiniMessage::class to MiniMessageParser, 83 | ) 84 | 85 | @Suppress("UNCHECKED_CAST") 86 | inline fun of(): ArgumentParser? = this.parsers[T::class] as? ArgumentParser 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/util/net/http/YggdrasilApiUtil.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.util.net.http 2 | 3 | import com.velocitypowered.api.util.UuidUtils 4 | import kotlinx.serialization.json.jsonObject 5 | import kotlinx.serialization.json.jsonPrimitive 6 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.logger 7 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 8 | import work.msdnicrosoft.avm.config.ConfigManager 9 | import work.msdnicrosoft.avm.config.data.Whitelist 10 | import work.msdnicrosoft.avm.util.file.FileUtil.JSON 11 | import java.net.URI 12 | import java.net.http.HttpClient 13 | import java.net.http.HttpRequest 14 | import java.net.http.HttpResponse 15 | import java.util.UUID 16 | import kotlin.String 17 | 18 | object YggdrasilApiUtil { 19 | private inline val config: Whitelist get() = ConfigManager.config.whitelist 20 | private val httpClient: HttpClient = HttpClient.newHttpClient() 21 | 22 | inline val serverIsOnlineMode: Boolean get() = server.configuration.isOnlineMode 23 | 24 | /** 25 | * Retrieves the username associated with the given [uuid]. 26 | * If the server is in offline mode, it returns null. 27 | * If the server is online, a query is made to the API to retrieve the username. 28 | */ 29 | fun getUsername(uuid: UUID): String? { 30 | if (!this.serverIsOnlineMode) return null 31 | 32 | val request: HttpRequest = HttpRequest.newBuilder() 33 | .setHeader("User-Agent", HttpUtil.USER_AGENT) 34 | .uri(URI.create("${config.queryApi.profile.trimEnd('/')}/${UuidUtils.toUndashed(uuid)}")) 35 | .build() 36 | 37 | return this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) 38 | .thenApply { resp: HttpResponse -> 39 | when (val status: HttpStatus = HttpStatus.fromValue(resp.statusCode())) { 40 | HttpStatus.OK -> JSON.parseToJsonElement(resp.body()).jsonObject["name"]?.jsonPrimitive.toString() 41 | HttpStatus.NOT_FOUND, HttpStatus.NO_CONTENT -> HttpStatus.NOT_FOUND.description 42 | HttpStatus.TOO_MANY_REQUESTS -> { 43 | logger.warn("Exceeded to the rate limit of Profile API, please retry UUID {}", uuid) 44 | null 45 | } 46 | 47 | else -> { 48 | logger.warn("Failed to query UUID {}: {} {}", uuid, status.value, status.description) 49 | null 50 | } 51 | } 52 | }.get() 53 | } 54 | 55 | /** 56 | * Retrieves the UUID associated with the given [username]. 57 | * 58 | * @param onlineMode Optional parameter to specify whether to use online mode or not. Defaults to null. 59 | */ 60 | fun getUuid(username: String, onlineMode: Boolean? = null): String? { 61 | if (onlineMode == false && !this.serverIsOnlineMode) { 62 | return UuidUtils.toUndashed(UuidUtils.generateOfflinePlayerUuid(username)) 63 | } 64 | 65 | val request: HttpRequest = HttpRequest.newBuilder() 66 | .setHeader("User-Agent", HttpUtil.USER_AGENT) 67 | .uri(URI.create("${config.queryApi.uuid.trimEnd('/')}/$username")) 68 | .build() 69 | 70 | return this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) 71 | .thenApply { resp: HttpResponse -> 72 | when (val status: HttpStatus = HttpStatus.fromValue(resp.statusCode())) { 73 | HttpStatus.OK -> JSON.parseToJsonElement(resp.body()).jsonObject["id"]?.jsonPrimitive.toString() 74 | HttpStatus.NOT_FOUND -> HttpStatus.NOT_FOUND.description 75 | HttpStatus.TOO_MANY_REQUESTS -> { 76 | logger.warn("Exceeded to the rate limit of UUID API, please retry username {}", username) 77 | null 78 | } 79 | 80 | else -> { 81 | logger.warn("Failed to query username {}: {} {}", username, status.value, status.description) 82 | null 83 | } 84 | } 85 | }.get() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/whitelist/AddCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.whitelist 2 | 3 | import net.kyori.adventure.text.Component 4 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 5 | import work.msdnicrosoft.avm.config.ConfigManager 6 | import work.msdnicrosoft.avm.module.whitelist.PlayerCache 7 | import work.msdnicrosoft.avm.module.whitelist.WhitelistManager 8 | import work.msdnicrosoft.avm.module.whitelist.result.AddResult 9 | import work.msdnicrosoft.avm.util.command.builder.* 10 | import work.msdnicrosoft.avm.util.command.context.CommandContext 11 | import work.msdnicrosoft.avm.util.command.data.server.Server 12 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 13 | import work.msdnicrosoft.avm.util.net.http.YggdrasilApiUtil 14 | import work.msdnicrosoft.avm.util.server.task 15 | import work.msdnicrosoft.avm.util.string.isUuid 16 | import work.msdnicrosoft.avm.util.string.toUuid 17 | 18 | object AddCommand { 19 | private inline val config get() = ConfigManager.config.whitelist 20 | 21 | val command = literalCommand("add") { 22 | requires { hasPermission("avm.command.whitelist.add") } 23 | wordArgument("player") { 24 | suggests { builder -> 25 | PlayerCache.readOnly.forEach(builder::suggest) 26 | server.allPlayers.forEach { builder.suggest(it.username) } 27 | WhitelistManager.usernames.forEach(builder::suggest) 28 | builder.buildFuture() 29 | } 30 | wordArgument("server") { 31 | suggests { builder -> 32 | val player: String by this 33 | val whitelistedServers = if (player.isUuid()) { 34 | WhitelistManager.getPlayer(player.toUuid()) 35 | } else { 36 | WhitelistManager.getPlayer(player) 37 | }?.serverList 38 | buildSet { 39 | addAll(config.serverGroups.keys) 40 | addAll(server.allServers.map { it.serverInfo.name }) 41 | }.filterNot { 42 | whitelistedServers?.contains(it) == true 43 | }.forEach(builder::suggest) 44 | builder.buildFuture() 45 | } 46 | executes { 47 | val player: String by this 48 | val server: Server by this 49 | addPlayer(player, server.name) 50 | Command.SINGLE_SUCCESS 51 | } 52 | boolArgument("onlineMode") { 53 | executes { 54 | val player: String by this 55 | val server: Server by this 56 | val onlineMode: Boolean by this 57 | task { addPlayer(player, server.name, onlineMode) } 58 | Command.SINGLE_SUCCESS 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | private fun CommandContext.addPlayer(player: String, serverName: String, onlineMode: Boolean? = null) { 66 | val isUuid: Boolean = player.isUuid() 67 | if (isUuid && !YggdrasilApiUtil.serverIsOnlineMode) { 68 | sendTranslatable("avm.command.avmwl.add.uuid_unsupported") 69 | return 70 | } 71 | 72 | val result: AddResult = if (isUuid) { 73 | WhitelistManager.add(player.toUuid(), serverName, onlineMode) 74 | } else { 75 | WhitelistManager.add(player, serverName, onlineMode) 76 | } 77 | 78 | val message: Component = when (result) { 79 | AddResult.SUCCESS -> tr("avm.command.avmwl.add.success") { 80 | args { 81 | string("player", player) 82 | string("server", serverName) 83 | } 84 | } 85 | 86 | AddResult.API_LOOKUP_NOT_FOUND -> tr("avm.command.avmwl.add.request.not_found") 87 | AddResult.API_LOOKUP_REQUEST_FAILED -> tr("avm.command.avmwl.add.request.failed") 88 | AddResult.ALREADY_EXISTS -> tr("avm.command.avmwl.add.already_exists") 89 | AddResult.SAVE_FILE_FAILED -> tr("avm.whitelist.save.failed") 90 | } 91 | sendMessage(message) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/EventBroadcast.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module 2 | 3 | import com.velocitypowered.api.event.PostOrder 4 | import com.velocitypowered.api.event.Subscribe 5 | import com.velocitypowered.api.event.connection.DisconnectEvent 6 | import com.velocitypowered.api.event.player.ServerConnectedEvent 7 | import com.velocitypowered.api.proxy.server.RegisteredServer 8 | import net.kyori.adventure.text.Component 9 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.eventManager 10 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 11 | import work.msdnicrosoft.avm.config.ConfigManager 12 | import work.msdnicrosoft.avm.util.component.builder.minimessage.miniMessage 13 | import work.msdnicrosoft.avm.util.server.nickname 14 | import work.msdnicrosoft.avm.util.server.task 15 | 16 | object EventBroadcast { 17 | private inline val config get() = ConfigManager.config.broadcast 18 | 19 | fun init() { 20 | eventManager.register(plugin, this) 21 | } 22 | 23 | fun disable() { 24 | eventManager.unregisterListener(plugin, this) 25 | } 26 | 27 | @Subscribe(order = PostOrder.FIRST) 28 | fun onPlayerDisconnect(event: DisconnectEvent) { 29 | if (!config.leave.enabled) return 30 | 31 | // If a player failed to join the server (due to an incompatible server version, etc.), 32 | // the plugin will send the leave message accidentally. 33 | // To avoid this, we check the login status. 34 | if (event.loginStatus != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) return 35 | 36 | sendMessage( 37 | miniMessage(config.leave.message) { 38 | placeholders { unparsed("player_name", event.player.username) } 39 | } 40 | ) 41 | 42 | if (config.leave.logging) { 43 | Logging.log("[-] ${event.player.username} left the server") 44 | } 45 | } 46 | 47 | @Subscribe(order = PostOrder.FIRST) 48 | fun onPlayerConnected(event: ServerConnectedEvent) { 49 | val username: String = event.player.username 50 | val targetServerName: String = event.server.serverInfo.name 51 | val targetServerNickname: String = event.server.serverInfo.nickname 52 | 53 | event.previousServer.ifPresentOrElse( 54 | { previousServer: RegisteredServer -> 55 | if (!config.switch.enabled) return@ifPresentOrElse 56 | 57 | if (previousServer == event.server) return@ifPresentOrElse 58 | 59 | val previousServerName: String = previousServer.serverInfo.name 60 | val previousServerNickname: String = previousServer.serverInfo.nickname 61 | 62 | this.sendMessage( 63 | miniMessage(config.switch.message) { 64 | placeholders { 65 | unparsed("player_name", username) 66 | unparsed("previous_server_name", previousServerName) 67 | unparsed("previous_server_nickname", previousServerNickname) 68 | unparsed("target_server_nickname", targetServerNickname) 69 | } 70 | } 71 | ) 72 | 73 | if (config.switch.logging) { 74 | Logging.log("[⇄] $username: $previousServerName ➟ $targetServerName") 75 | } 76 | }, 77 | { 78 | if (!config.join.enabled) return@ifPresentOrElse 79 | 80 | this.sendMessage( 81 | miniMessage(config.join.message) { 82 | placeholders { 83 | unparsed("player_name", username) 84 | unparsed("server_name", targetServerName) 85 | unparsed("server_nickname", targetServerNickname) 86 | } 87 | } 88 | ) 89 | 90 | if (config.join.logging) { 91 | Logging.log("[+] $username joined server $targetServerName") 92 | } 93 | } 94 | ) 95 | } 96 | 97 | private fun sendMessage(message: Component) = task { 98 | plugin.server.allPlayers.parallelStream() 99 | .forEach { player -> player.sendMessage(message) } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/AVMCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command 2 | 3 | import com.velocitypowered.api.util.ProxyVersion 4 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.plugin 5 | import work.msdnicrosoft.avm.annotations.command.CommandNode 6 | import work.msdnicrosoft.avm.annotations.command.RootCommand 7 | import work.msdnicrosoft.avm.command.utility.* 8 | import work.msdnicrosoft.avm.module.command.session.CommandSessionManager 9 | import work.msdnicrosoft.avm.module.command.session.ExecuteResult 10 | import work.msdnicrosoft.avm.util.command.builder.* 11 | import work.msdnicrosoft.avm.util.command.context.buildHelp 12 | import work.msdnicrosoft.avm.util.command.register 13 | import work.msdnicrosoft.avm.util.command.unregister 14 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 15 | import work.msdnicrosoft.avm.util.server.task 16 | import kotlin.time.Duration 17 | import kotlin.time.measureTimedValue 18 | 19 | @RootCommand("avm") 20 | object AVMCommand { 21 | 22 | @CommandNode("reload") 23 | val reload = literalCommand("reload") { 24 | requires { hasPermission("avm.command.reload") } 25 | executes { 26 | val (success: Boolean, elapsed: Duration) = measureTimedValue { plugin.reload() } 27 | if (success) { 28 | sendTranslatable("avm.command.avm.reload.success") { 29 | args { numeric("elapsed", elapsed.inWholeMilliseconds) } 30 | } 31 | } else { 32 | sendTranslatable("avm.command.avm.reload.failed") 33 | } 34 | Command.SINGLE_SUCCESS 35 | } 36 | } 37 | 38 | @CommandNode("info") 39 | val info = literalCommand("info") { 40 | requires { hasPermission("avm.command.info") } 41 | executes { 42 | val velocity: ProxyVersion = plugin.server.version 43 | sendTranslatable("avm.command.avm.info.plugin.name") { 44 | args { component("name", tr("avm.general.plugin.name")) } 45 | } 46 | sendTranslatable("avm.command.avm.info.plugin.version") { 47 | args { string("version", plugin.self.version.get()) } 48 | } 49 | sendTranslatable("avm.command.avm.info.server") { 50 | args { string("server", "${velocity.name} ${velocity.version}") } 51 | } 52 | Command.SINGLE_SUCCESS 53 | } 54 | } 55 | 56 | @CommandNode("confirm", "") 57 | val confirm = literalCommand("confirm") { 58 | requires { hasPermission("avm.command.confirm") } 59 | greedyStringArgument("session") { 60 | executes { 61 | val session: String by this 62 | task { 63 | val result: String = when (CommandSessionManager.executeAction(session)) { 64 | ExecuteResult.SUCCESS -> return@task 65 | ExecuteResult.EXPIRED -> "avm.command.avm.confirm.expired" 66 | ExecuteResult.FAILED -> "avm.command.avm.confirm.failed" 67 | ExecuteResult.NOT_FOUND -> "avm.command.avm.confirm.not_found" 68 | } 69 | sendTranslatable(result) 70 | } 71 | Command.SINGLE_SUCCESS 72 | } 73 | } 74 | } 75 | 76 | @CommandNode("import", "", "") 77 | val import = ImportCommand.command 78 | 79 | @CommandNode("kick", "", "[reason]") 80 | val kick = KickCommand.command 81 | 82 | @CommandNode("kickall", "[server]", "[reason]") 83 | val kickAll = KickAllCommand.command 84 | 85 | @CommandNode("send", "", "", "[reason]") 86 | val send = SendCommand.command 87 | 88 | @CommandNode("sendall", "", "[reason]") 89 | val sendAll = SendAllCommand.command 90 | 91 | val command = literalCommand("avm") { 92 | executes { buildHelp(this@AVMCommand.javaClass) } 93 | then(reload) 94 | then(info) 95 | then(confirm) 96 | then(import) 97 | then(kickAll) 98 | then(kick) 99 | then(sendAll) 100 | then(send) 101 | }.build() 102 | 103 | fun init() { 104 | this.command.register() 105 | } 106 | 107 | fun disable() { 108 | this.command.unregister() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/i18n/TranslateManager.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.i18n 2 | 3 | import com.highcapable.kavaref.extension.classOf 4 | import net.kyori.adventure.key.Key 5 | import net.kyori.adventure.text.minimessage.translation.MiniMessageTranslator 6 | import net.kyori.adventure.translation.GlobalTranslator 7 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.dataDirectory 8 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.logger 9 | import work.msdnicrosoft.avm.util.file.FileUtil.JSON 10 | import work.msdnicrosoft.avm.util.file.readTextWithBuffer 11 | import java.io.FileOutputStream 12 | import java.nio.file.Path 13 | import java.util.* 14 | import java.util.concurrent.ConcurrentHashMap 15 | import java.util.jar.JarFile 16 | import kotlin.io.path.* 17 | 18 | object TranslateManager : MiniMessageTranslator() { 19 | private val NAME: Key = Key.key("avm") 20 | private val DEFAULT_LOCALE: Locale = Locale.forLanguageTag("en_US") 21 | 22 | private val globalTranslator: GlobalTranslator = GlobalTranslator.translator() 23 | private val languageFileDirectory: Path = dataDirectory / "lang" 24 | 25 | private val translations: MutableMap> = mutableMapOf() 26 | 27 | fun init() { 28 | logger.info("Loading language...") 29 | this.registerTranslations() 30 | this.globalTranslator.addSource(this) 31 | } 32 | 33 | fun disable() { 34 | this.globalTranslator.removeSource(this) 35 | } 36 | 37 | fun reload() { 38 | logger.info("Reloading language...") 39 | this.translations.clear() 40 | this.registerTranslations() 41 | } 42 | 43 | override fun name(): Key = this.NAME 44 | 45 | override fun getMiniMessageString(key: String, locale: Locale): String? { 46 | val currentLocale = this.translations[locale] 47 | ?: this.translations[Locale.forLanguageTag(locale.language)] 48 | ?: this.translations[this.DEFAULT_LOCALE] 49 | return currentLocale?.get(key) 50 | } 51 | 52 | @Suppress("NestedBlockDepth") 53 | private fun checkAndUpdateTranslations() { 54 | val jarUrl = classOf().protectionDomain.codeSource?.location ?: return 55 | JarFile(jarUrl.path).use { jarFile -> 56 | jarFile.entries().asSequence() 57 | .filter { entry -> entry.name.startsWith("lang/") && !entry.isDirectory } 58 | .forEach { entry -> 59 | val outPath = (this.languageFileDirectory / entry.name.removePrefix("lang/")).toFile() 60 | outPath.parentFile.mkdirs() 61 | 62 | val fileExists = outPath.exists() 63 | val fileContentChanged = if (fileExists) { 64 | val localContent = outPath.readTextWithBuffer() 65 | val jarContent = jarFile.getInputStream(entry).bufferedReader().use { it.readText() } 66 | localContent != jarContent 67 | } else { 68 | true 69 | } 70 | 71 | if (fileExists && !fileContentChanged) { 72 | return@forEach 73 | } 74 | 75 | jarFile.getInputStream(entry).use { input -> 76 | FileOutputStream(outPath).use { output -> 77 | input.copyTo(output) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | private fun getLanguageFiles(): List { 85 | this.languageFileDirectory.toFile().mkdirs() 86 | return this.languageFileDirectory.listDirectoryEntries().filter { entry -> 87 | entry.extension.equals("json", ignoreCase = true) && entry.isRegularFile() 88 | } 89 | } 90 | 91 | private fun registerTranslations() { 92 | if (this.getLanguageFiles().isEmpty()) { 93 | this.checkAndUpdateTranslations() 94 | } 95 | 96 | this.getLanguageFiles().forEach { file -> 97 | val locale = Locale.forLanguageTag(file.nameWithoutExtension) 98 | val currentTranslations: Map = JSON.decodeFromString(file.readTextWithBuffer()) 99 | this.translations.computeIfAbsent(locale) { ConcurrentHashMap() }.putAll(currentTranslations) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/whitelist/WhitelistHandler.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.whitelist 2 | 3 | import com.highcapable.kavaref.KavaRef.Companion.resolve 4 | import com.highcapable.kavaref.extension.classOf 5 | import com.highcapable.kavaref.resolver.FieldResolver 6 | import com.velocitypowered.api.event.PostOrder 7 | import com.velocitypowered.api.event.Subscribe 8 | import com.velocitypowered.api.event.connection.LoginEvent 9 | import com.velocitypowered.api.event.connection.PreLoginEvent 10 | import com.velocitypowered.api.event.player.ServerPreConnectEvent 11 | import com.velocitypowered.api.proxy.InboundConnection 12 | import com.velocitypowered.proxy.connection.client.InitialInboundConnection 13 | import io.netty.channel.Channel 14 | import io.netty.util.AttributeKey 15 | import net.kyori.adventure.text.Component 16 | import org.geysermc.floodgate.api.player.FloodgatePlayer 17 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 18 | import work.msdnicrosoft.avm.config.ConfigManager 19 | import work.msdnicrosoft.avm.util.component.builder.minimessage.miniMessage 20 | 21 | /** 22 | * Portions of this code are modified from lls-manager 23 | * https://github.com/plusls/lls-manager/blob/master/src/main/java/com/plusls/llsmanager/whitelist/WhitelistHandler.java 24 | */ 25 | object WhitelistHandler { 26 | private inline val config get() = ConfigManager.config.whitelist 27 | 28 | private val delegateFieldResolver: FieldResolver by lazy { 29 | classOf().resolve().firstField { name = "delegate" } 30 | } 31 | 32 | private val hasFloodgate: Boolean by lazy { server.pluginManager.getPlugin("floodgate").isPresent } 33 | 34 | @Subscribe(order = PostOrder.EARLY) 35 | fun onPreLogin(event: PreLoginEvent) { 36 | // Blocked by other plugins or whitelist is off 37 | if (!event.result.isAllowed || !config.enabled) return 38 | 39 | val username: String = event.connection.getJavaUsernameOrDefault(event.username) 40 | val player = WhitelistManager.getPlayer(username) 41 | if (player == null) { 42 | event.result = PreLoginEvent.PreLoginComponentResult.denied(miniMessage(config.message)) 43 | PlayerCache.add(username) 44 | } else { 45 | event.result = if (player.onlineMode) { 46 | PreLoginEvent.PreLoginComponentResult.forceOnlineMode() 47 | } else { 48 | PreLoginEvent.PreLoginComponentResult.forceOfflineMode() 49 | } 50 | } 51 | } 52 | 53 | @Subscribe 54 | fun onPlayerLogin(event: LoginEvent) { 55 | WhitelistManager.updatePlayer(event.player.username, event.player.uniqueId) 56 | } 57 | 58 | @Subscribe(order = PostOrder.EARLY) 59 | fun onServerPreConnect(event: ServerPreConnectEvent) { 60 | // Blocked by other plugins or whitelist is off 61 | if (event.result.server.isEmpty || !config.enabled) return 62 | 63 | val serverName: String = event.originalServer.serverInfo.name 64 | val player = event.player 65 | 66 | if (!WhitelistManager.isListed(player.uniqueId, serverName)) { 67 | event.result = ServerPreConnectEvent.ServerResult.denied() 68 | val message: Component = miniMessage(config.message) 69 | player.sendMessage(message) 70 | if (event.previousServer == null) { 71 | player.disconnect(message) 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Retrieves the Java username associated with the connection when Floodgate is enabled and the user is linked. 78 | * If Floodgate is not enabled or the user is not linked, returns the provided default [username]. 79 | */ 80 | @Suppress("UnsafeCallOnNullableType") 81 | private fun InboundConnection.getJavaUsernameOrDefault(username: String): String { 82 | // Compatible with Floodgate 83 | if (this@WhitelistHandler.hasFloodgate) { 84 | val channel: Channel = this@WhitelistHandler.delegateFieldResolver.copy() 85 | .of(this) 86 | .get()!!.connection.channel 87 | val player: FloodgatePlayer? = channel 88 | .attr(AttributeKey.valueOf("floodgate-player")) 89 | .get() 90 | if (player?.isLinked == true) { 91 | return player.linkedPlayer.javaUsername 92 | } 93 | } 94 | return username 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/reconnect/Reconnection.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.reconnect 2 | 3 | import com.velocitypowered.api.event.Continuation 4 | import com.velocitypowered.api.event.player.KickedFromServerEvent 5 | import com.velocitypowered.api.proxy.server.PingOptions 6 | import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler 7 | import com.velocitypowered.proxy.connection.client.ConnectedPlayer 8 | import com.velocitypowered.proxy.protocol.packet.BossBarPacket 9 | import io.netty.channel.EventLoop 10 | import net.kyori.adventure.title.Title 11 | import work.msdnicrosoft.avm.config.ConfigManager 12 | import work.msdnicrosoft.avm.packet.s2c.PlayerAbilitiesPacket 13 | import work.msdnicrosoft.avm.util.component.builder.title 14 | import java.time.Duration 15 | import java.util.UUID 16 | import java.util.concurrent.TimeUnit 17 | import kotlin.time.Duration.Companion.seconds 18 | 19 | @Suppress("MagicNumber") 20 | class Reconnection(private val event: KickedFromServerEvent, private val continuation: Continuation) { 21 | private val player = event.player as ConnectedPlayer 22 | private val scheduledExecutor: EventLoop = player.connection.eventLoop() 23 | 24 | private val pingOptions: PingOptions = PingOptions.builder() 25 | .timeout(Duration.ofMillis(config.pingTimeout)) 26 | .build() 27 | 28 | private val connectingTitle: Title = title { 29 | mainTitle { mini(config.message.connecting.title) } 30 | subTitle { mini(config.message.connecting.subtitle) } 31 | fadeIn(1L.seconds) 32 | stay(30L.seconds) 33 | fadeOut(1L.seconds) 34 | } 35 | 36 | private val waitingTitle: Title = title { 37 | mainTitle { mini(config.message.waiting.title) } 38 | subTitle { mini(config.message.waiting.subtitle) } 39 | fadeIn(1L.seconds) 40 | stay(30L.seconds) 41 | fadeOut(1L.seconds) 42 | } 43 | 44 | private var state: State = State.WAITING 45 | 46 | init { 47 | // Prevent player to be kicked by no-flight 48 | this.player.connection.write(PlayerAbilitiesPacket(PlayerAbilitiesPacket.NO_FALLING)) 49 | 50 | this.player.tabList.clearAll() 51 | clearBossBars() 52 | } 53 | 54 | fun reconnect() { 55 | scheduleConnect() 56 | scheduleSendMessage() 57 | } 58 | 59 | private fun scheduleConnect() { 60 | this.scheduledExecutor.schedule(this::connect, config.pingInterval, TimeUnit.MILLISECONDS) 61 | } 62 | 63 | private fun scheduleSendMessage() { 64 | this.scheduledExecutor.schedule(this::sendMessage, config.messageInterval, TimeUnit.MILLISECONDS) 65 | } 66 | 67 | private fun connect() { 68 | if (this.state == State.CONNECTED) return 69 | this.event.server.ping(this.pingOptions).whenComplete { _, throwable -> 70 | if (throwable != null) { 71 | this.scheduleConnect() 72 | } else { 73 | this.scheduledExecutor.execute { 74 | this.state = State.CONNECTING 75 | this.scheduledExecutor.schedule({ 76 | this.player.clearTitle() 77 | this.event.result = KickedFromServerEvent.RedirectPlayer.create(this.event.server) 78 | this.state = State.CONNECTED 79 | this.continuation.resume() 80 | }, config.reconnectDelay, TimeUnit.MILLISECONDS) 81 | } 82 | } 83 | } 84 | } 85 | 86 | private fun sendMessage() { 87 | if (this.state == State.CONNECTED) return 88 | 89 | this.player.showTitle(if (this.state == State.CONNECTING) this.connectingTitle else this.waitingTitle) 90 | scheduleSendMessage() 91 | } 92 | 93 | private fun clearBossBars() { 94 | val sessionHandler = this.player.connection.activeSessionHandler as? ClientPlaySessionHandler ?: return 95 | 96 | sessionHandler.serverBossBars.forEach { bossBar: UUID -> 97 | this.player.connection.delayedWrite( 98 | BossBarPacket().apply { 99 | uuid = bossBar 100 | action = BossBarPacket.REMOVE 101 | } 102 | ) 103 | } 104 | sessionHandler.serverBossBars.clear() 105 | } 106 | 107 | companion object { 108 | private enum class State { WAITING, CONNECTING, CONNECTED } 109 | 110 | private inline val config get() = ConfigManager.config.reconnect 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/chatbridge/MsgCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.chatbridge 2 | 3 | import com.velocitypowered.api.command.CommandSource 4 | import com.velocitypowered.api.proxy.Player 5 | import net.kyori.adventure.text.Component 6 | import net.kyori.adventure.text.JoinConfiguration 7 | import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver 8 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 9 | import work.msdnicrosoft.avm.config.ConfigManager 10 | import work.msdnicrosoft.avm.util.DateTimeUtil.getDateTime 11 | import work.msdnicrosoft.avm.util.command.builder.* 12 | import work.msdnicrosoft.avm.util.command.context.isConsole 13 | import work.msdnicrosoft.avm.util.command.context.name 14 | import work.msdnicrosoft.avm.util.command.context.toPlayer 15 | import work.msdnicrosoft.avm.util.command.register 16 | import work.msdnicrosoft.avm.util.command.unregister 17 | import work.msdnicrosoft.avm.util.component.ComponentSerializer 18 | import work.msdnicrosoft.avm.util.component.Format 19 | import work.msdnicrosoft.avm.util.component.builder.minimessage.miniMessage 20 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.placeholders 21 | import work.msdnicrosoft.avm.util.component.builder.style.styled 22 | 23 | object MsgCommand { 24 | private inline val chatFormat get() = ConfigManager.config.chatBridge.privateChatFormat 25 | private inline val shouldTakeOverPrivateChat: Boolean get() = ConfigManager.config.chatBridge.takeOverPrivateChat 26 | private inline val allowFormatCode: Boolean get() = ConfigManager.config.chatBridge.allowFormatCode 27 | 28 | val aliases: List = listOf("msg", "tell", "w") 29 | 30 | val command = literalCommand("msg") { 31 | stringArgument("targets") { 32 | suggests { builder -> 33 | val players: Collection = if (shouldTakeOverPrivateChat || context.source.isConsole) { 34 | server.allPlayers 35 | } else { 36 | context.source.toPlayer().currentServer.get().server.playersConnected 37 | } 38 | players.forEach { builder.suggest(it.username) } 39 | builder.buildFuture() 40 | } 41 | greedyStringArgument("message") { 42 | executes { 43 | val targets: Player by this 44 | val message: String by this 45 | 46 | if (!context.source.isConsole) { 47 | sendMessage(chatFormat.sender.buildMessage(context.source, targets, message)) 48 | } 49 | targets.sendMessage(chatFormat.receiver.buildMessage(context.source, targets, message)) 50 | Command.SINGLE_SUCCESS 51 | } 52 | } 53 | } 54 | }.build() 55 | 56 | fun init() { 57 | this.command.register("tell", "w") 58 | } 59 | 60 | fun disable() { 61 | this.command.unregister() 62 | } 63 | 64 | private fun List.buildMessage(source: CommandSource, player: Player, message: String): Component { 65 | val time: String = getDateTime() 66 | val tagResolvers: List = placeholders { 67 | unparsed("player_name_from", source.name) 68 | unparsed("player_name_to", player.username) 69 | if (allowFormatCode) { 70 | parsed("player_message", message) 71 | } else { 72 | unparsed("player_message", message) 73 | } 74 | unparsed("player_message_sent_time", time) 75 | } 76 | return Component.join( 77 | JoinConfiguration.noSeparators(), 78 | this.map { format -> 79 | miniMessage(format.text, provider = ComponentSerializer.STYLE_ONLY_MINI_MESSAGE) { 80 | placeholders { tagResolvers(tagResolvers) } 81 | } styled { 82 | hoverText { 83 | miniMessage(format.hover?.joinToString("\n").orEmpty()) { 84 | placeholders { tagResolvers(tagResolvers) } 85 | } 86 | } 87 | click(format.applyReplace { replacePlaceHolders(source.name, player.username, time) }) 88 | } 89 | } 90 | ) 91 | } 92 | 93 | private fun String.replacePlaceHolders(from: String, to: String, time: String): String = this 94 | .replace("", from) 95 | .replace("", to) 96 | .replace("", time) 97 | } 98 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/command/whitelist/RemoveCommand.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.command.whitelist 2 | 3 | import com.velocitypowered.api.command.CommandSource 4 | import com.velocitypowered.api.proxy.Player 5 | import net.kyori.adventure.text.Component 6 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.server 7 | import work.msdnicrosoft.avm.config.ConfigManager 8 | import work.msdnicrosoft.avm.module.whitelist.WhitelistManager 9 | import work.msdnicrosoft.avm.module.whitelist.result.RemoveResult 10 | import work.msdnicrosoft.avm.util.command.builder.* 11 | import work.msdnicrosoft.avm.util.command.data.server.Server 12 | import work.msdnicrosoft.avm.util.component.builder.minimessage.tag.tr 13 | import work.msdnicrosoft.avm.util.server.ProxyServerUtil.kickPlayers 14 | import work.msdnicrosoft.avm.util.server.task 15 | import work.msdnicrosoft.avm.util.string.isUuid 16 | import work.msdnicrosoft.avm.util.string.toUuid 17 | import java.util.Optional 18 | 19 | object RemoveCommand { 20 | private inline val config get() = ConfigManager.config.whitelist 21 | 22 | val command = literalCommand("remove") { 23 | requires { hasPermission("avm.command.whitelist.remove") } 24 | wordArgument("player") { 25 | suggests { builder -> 26 | WhitelistManager.usernames.forEach(builder::suggest) 27 | builder.buildFuture() 28 | } 29 | executes { 30 | val player: String by this 31 | context.source.removePlayer(player) 32 | Command.SINGLE_SUCCESS 33 | } 34 | wordArgument("server") { 35 | suggests { builder -> 36 | val player: String by this 37 | val serverList = if (player.isUuid()) { 38 | WhitelistManager.getPlayer(player.toUuid()) 39 | } else { 40 | WhitelistManager.getPlayer(player) 41 | }?.serverList 42 | serverList?.forEach(builder::suggest) 43 | builder.buildFuture() 44 | } 45 | executes { 46 | val server: Server by this 47 | val player: String by this 48 | context.source.removePlayer(player, server.name) 49 | Command.SINGLE_SUCCESS 50 | } 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Removes a player from the whitelist. 57 | * 58 | * @param playerName The name or UUID of the player to remove. 59 | * @param serverName The name of the server to remove the player from. 60 | * If null, the player will be removed from all servers. 61 | */ 62 | private fun CommandSource.removePlayer(playerName: String, serverName: String? = null) { 63 | val isUuid: Boolean = playerName.isUuid() 64 | val result: RemoveResult = if (isUuid) { 65 | WhitelistManager.remove(playerName.toUuid(), serverName) 66 | } else { 67 | WhitelistManager.remove(playerName, serverName) 68 | } 69 | 70 | val message: Component = when (result) { 71 | RemoveResult.SUCCESS -> { 72 | if (serverName != null) { 73 | tr("avm.command.avmwl.remove.success.server") { 74 | args { 75 | string("server", serverName) 76 | string("player", playerName) 77 | } 78 | } 79 | } else { 80 | tr("avm.command.avmwl.remove.success.full") { 81 | args { string("player", playerName) } 82 | } 83 | } 84 | } 85 | 86 | RemoveResult.FAIL_NOT_FOUND -> tr("avm.command.avmwl.remove.not_found") 87 | RemoveResult.SAVE_FILE_FAILED -> tr("avm.whitelist.save.failed") 88 | } 89 | this.sendMessage(message) 90 | 91 | if (config.enabled) { 92 | task { 93 | val player: Optional = if (isUuid) { 94 | server.getPlayer(playerName.toUuid()) 95 | } else { 96 | server.getPlayer(playerName) 97 | } 98 | player.ifPresent { 99 | if (!WhitelistManager.isListed(it.uniqueId, it.currentServer.get().serverInfo.name)) { 100 | kickPlayers(config.message, it) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/kotlin/work/msdnicrosoft/avm/module/imports/importers/LlsManagerImporter.kt: -------------------------------------------------------------------------------- 1 | package work.msdnicrosoft.avm.module.imports.importers 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import work.msdnicrosoft.avm.AdvancedVelocityManagerPlugin.Companion.dataDirectory 6 | import work.msdnicrosoft.avm.config.ConfigManager 7 | import work.msdnicrosoft.avm.module.whitelist.WhitelistManager 8 | import work.msdnicrosoft.avm.util.command.context.CommandContext 9 | import work.msdnicrosoft.avm.util.file.FileUtil.JSON 10 | import work.msdnicrosoft.avm.util.file.readTextWithBuffer 11 | import java.nio.file.Path 12 | import kotlin.io.path.* 13 | 14 | object LlsManagerImporter : Importer { 15 | @Serializable 16 | private data class Config( 17 | val showAllPlayerInTabList: Boolean, 18 | val bridgeChatMessage: Boolean, 19 | val bridgePlayerJoinMessage: Boolean, 20 | val bridgePlayerLeaveMessage: Boolean, 21 | 22 | @SerialName("whitelist") 23 | val whitelistEnabled: Boolean, 24 | 25 | val serverGroup: Map> 26 | ) 27 | 28 | @Serializable 29 | private data class PlayerData( 30 | @SerialName("whitelistServerList") 31 | val serverList: List, 32 | val onlineMode: Boolean 33 | ) 34 | 35 | private val PATH: Path = dataDirectory.parent / "lls-manager" 36 | private val CONFIG_PATH: Path = PATH / "config.json" 37 | private val PLAYER_DATA_PATH: Path = PATH / "player" 38 | 39 | override val displayName: String = "lls-manager" 40 | 41 | override fun import(context: CommandContext, defaultServer: String): Boolean { 42 | val configSuccess: Boolean = if (this.CONFIG_PATH.exists()) { 43 | context.importConfig() 44 | } else { 45 | context.sendTranslatable("avm.command.avm.import.config.not_exist") { 46 | args { string("plugin_name", displayName) } 47 | } 48 | true 49 | } 50 | 51 | val playerDataSuccess: Boolean = if (PLAYER_DATA_PATH.exists()) { 52 | context.importPlayerData(defaultServer) 53 | } else { 54 | context.sendTranslatable("avm.command.avm.import.player.not_exist") { 55 | args { string("plugin_name", displayName) } 56 | } 57 | true 58 | } 59 | 60 | return configSuccess && playerDataSuccess 61 | } 62 | 63 | private fun CommandContext.importConfig(): Boolean = 64 | try { 65 | val config = JSON.decodeFromString(CONFIG_PATH.readTextWithBuffer()) 66 | 67 | ConfigManager.config.apply { 68 | tabSync.enabled = config.showAllPlayerInTabList 69 | chatBridge.enabled = config.bridgeChatMessage 70 | broadcast.join.enabled = config.bridgePlayerJoinMessage 71 | broadcast.leave.enabled = config.bridgePlayerLeaveMessage 72 | whitelist.enabled = config.whitelistEnabled 73 | whitelist.serverGroups = config.serverGroup 74 | } 75 | ConfigManager.save() 76 | } catch (e: Exception) { 77 | sendTranslatable("avm.command.avm.import.config.failed") { 78 | args { 79 | string("plugin_name", displayName) 80 | string("reason", e.message.orEmpty()) 81 | } 82 | } 83 | false 84 | } 85 | 86 | private fun CommandContext.importPlayerData(defaultServer: String): Boolean { 87 | var success = true 88 | 89 | PLAYER_DATA_PATH.listDirectoryEntries().asSequence() 90 | .filter { file -> file.extension.equals("json", ignoreCase = true) && file.isRegularFile() } 91 | .forEach { file -> 92 | val username: String = file.nameWithoutExtension 93 | try { 94 | val llsPlayer = JSON.decodeFromString(file.readTextWithBuffer()) 95 | val servers: List = llsPlayer.serverList.ifEmpty { listOf(defaultServer) } 96 | servers.forEach { server -> 97 | WhitelistManager.add(username, server, llsPlayer.onlineMode) 98 | } 99 | } catch (e: Exception) { 100 | sendTranslatable("avm.command.avm.import.player.failed") { 101 | args { 102 | string("player", username) 103 | string("plugin_name", displayName) 104 | string("reason", e.message.orEmpty()) 105 | } 106 | } 107 | success = false 108 | } 109 | } 110 | return success 111 | } 112 | } 113 | --------------------------------------------------------------------------------