├── .github ├── CODEOWNERS ├── FUNDING.yml └── workflows │ ├── set-version.yml │ ├── gradle.yml │ └── release.yml ├── images ├── logo.png └── players.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── changelog.sh ├── bungee ├── src │ └── main │ │ ├── resources │ │ └── bungee.yml │ │ └── java │ │ └── net │ │ └── pistonmaster │ │ └── pistonqueue │ │ └── bungee │ │ ├── utils │ │ └── ChatUtils.java │ │ ├── commands │ │ ├── MainCommand.java │ │ └── BungeeComponentWrapperImpl.java │ │ ├── listeners │ │ └── QueueListenerBungee.java │ │ └── PistonQueueBungee.java └── build.gradle.kts ├── placeholder ├── src │ └── main │ │ ├── resources │ │ └── plugin.yml │ │ └── java │ │ └── net │ │ └── pistonmaster │ │ └── pistonqueue │ │ └── placeholder │ │ ├── PAPIExpansion.java │ │ └── PistonQueuePlaceholder.java └── build.gradle.kts ├── velocity ├── src │ └── main │ │ ├── resources │ │ └── velocity-plugin.json │ │ └── java │ │ └── net │ │ └── pistonmaster │ │ └── pistonqueue │ │ └── velocity │ │ ├── utils │ │ └── ChatUtils.java │ │ ├── commands │ │ ├── MainCommand.java │ │ └── VelocityComponentWrapperImpl.java │ │ └── listeners │ │ └── QueueListenerVelocity.java └── build.gradle.kts ├── bukkit ├── build.gradle.kts └── src │ └── main │ ├── resources │ ├── plugin.yml │ └── config.yml │ └── java │ └── net │ └── pistonmaster │ └── pistonqueue │ └── bukkit │ ├── QueuePluginMessageListener.java │ ├── ProtocolLibWrapper.java │ ├── config │ └── BukkitConfig.java │ └── ServerListener.java ├── renovate.json ├── gradle.properties ├── shared ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── net │ │ └── pistonmaster │ │ └── pistonqueue │ │ └── shared │ │ ├── chat │ │ ├── MessageType.java │ │ ├── TextDecorationWrapper.java │ │ ├── TextColorWrapper.java │ │ ├── ComponentWrapperFactory.java │ │ └── ComponentWrapper.java │ │ ├── wrapper │ │ ├── PermissibleWrapper.java │ │ ├── CommandSourceWrapper.java │ │ ├── ServerInfoWrapper.java │ │ └── PlayerWrapper.java │ │ ├── events │ │ ├── PQPreLoginEvent.java │ │ ├── PQServerConnectedEvent.java │ │ ├── PQKickedFromServerEvent.java │ │ └── PQServerPreConnectEvent.java │ │ ├── queue │ │ ├── logic │ │ │ ├── ShadowBanService.java │ │ │ ├── StorageShadowBanService.java │ │ │ ├── UsernameValidator.java │ │ │ ├── QueueAvailabilityCalculator.java │ │ │ ├── ShadowBanKickHandler.java │ │ │ ├── QueueMoveProcessor.java │ │ │ ├── QueueRecoveryHandler.java │ │ │ ├── QueueEntryFactory.java │ │ │ ├── QueueCleaner.java │ │ │ ├── KickEventHandler.java │ │ │ ├── QueuePlacementCoordinator.java │ │ │ ├── QueueEnvironment.java │ │ │ └── QueueConnector.java │ │ ├── BanType.java │ │ ├── QueueGroup.java │ │ ├── QueueType.java │ │ └── QueueListenerShared.java │ │ ├── config │ │ ├── StorageData.java │ │ └── ConfigMigrator.java │ │ ├── hooks │ │ └── PistonMOTDPlaceholder.java │ │ └── utils │ │ ├── SharedChatUtils.java │ │ └── StorageTool.java │ └── test │ └── java │ └── net │ └── pistonmaster │ └── pistonqueue │ └── shared │ └── queue │ └── logic │ ├── UsernameValidatorTest.java │ ├── ShadowBanKickHandlerTest.java │ ├── QueueAvailabilityCalculatorTest.java │ ├── QueueCleanerTest.java │ ├── QueueRecoveryHandlerTest.java │ ├── QueueConnectorTest.java │ ├── QueueEntryFactoryTest.java │ └── KickEventHandlerTest.java ├── .editorconfig ├── .gitignore ├── universal └── build.gradle.kts ├── settings.gradle.kts ├── README.md ├── gradlew.bat └── gradlew /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AlexProgrammerDE 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: alexprogrammerde 2 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexProgrammerDE/PistonQueue/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/players.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexProgrammerDE/PistonQueue/HEAD/images/players.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexProgrammerDE/PistonQueue/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git pull origin --tags 4 | git log --pretty=format:"%h %s" --no-merges $(git describe --tags --abbrev=0)..HEAD 5 | -------------------------------------------------------------------------------- /bungee/src/main/resources/bungee.yml: -------------------------------------------------------------------------------- 1 | name: PistonQueue 2 | main: net.pistonmaster.pistonqueue.bungee.PistonQueueBungee 3 | description: ${description} 4 | version: ${version} 5 | author: AlexProgrammerDE 6 | softDepends: [ "PistonMOTD" ] -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /placeholder/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: PistonQueuePlaceholder 2 | version: ${version} 3 | main: net.pistonmaster.pistonqueue.placeholder.PistonQueuePlaceholder 4 | api-version: 1.13 5 | authors: [ AlexProgrammerDE ] 6 | description: Adds queue data to Placeholder API 7 | website: https://pistonmaster.net/ 8 | depend: [ PlaceholderAPI ] -------------------------------------------------------------------------------- /bungee/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("pq.platform-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(projects.pistonqueueShared) 7 | 8 | implementation("net.pistonmaster:PistonUtils:1.4.0") 9 | implementation("org.bstats:bstats-bungeecord:3.1.0") 10 | 11 | compileOnly("net.md-5:bungeecord-api:1.21-R0.4") 12 | } 13 | -------------------------------------------------------------------------------- /velocity/src/main/resources/velocity-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "pistonqueue", 3 | "name": "PistonQueue", 4 | "version": "${version}", 5 | "description": "${description}", 6 | "url": "${url}", 7 | "authors": [ 8 | "AlexProgrammerDE" 9 | ], 10 | "dependencies": [], 11 | "main": "net.pistonmaster.pistonqueue.velocity.PistonQueueVelocity" 12 | } 13 | -------------------------------------------------------------------------------- /bukkit/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("pq.platform-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation("net.pistonmaster:PistonUtils:1.4.0") 7 | implementation("org.bstats:bstats-bukkit:3.1.0") 8 | implementation("de.exlll:configlib-yaml:4.7.0") 9 | 10 | compileOnly("net.dmulloy2:ProtocolLib:5.4.0") 11 | compileOnly("org.spigotmc:spigot-api:1.18.1-R0.1-SNAPSHOT") 12 | } 13 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "group:allNonMajor", ":semanticCommits"], 3 | "packageRules": [ 4 | { 5 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 6 | "automerge": true, 7 | "automergeType": "branch" 8 | }, 9 | { 10 | "matchPackageNames": [ 11 | "**" 12 | ], 13 | "allowedVersions": "!/\\-SNAPSHOT$/" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /placeholder/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("pq.platform-conventions") 3 | } 4 | 5 | dependencies { 6 | compileOnly("org.spigotmc:spigot-api:1.18.1-R0.1-SNAPSHOT") 7 | compileOnly("me.clip:placeholderapi:2.11.7") 8 | 9 | compileOnly("org.projectlombok:lombok:1.18.42") 10 | annotationProcessor("org.projectlombok:lombok:1.18.42") 11 | 12 | implementation("net.pistonmaster:PistonUtils:1.4.0") 13 | } 14 | -------------------------------------------------------------------------------- /bukkit/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: PistonQueueBukkit 2 | version: ${version} 3 | api-version: 1.13 4 | main: net.pistonmaster.pistonqueue.bukkit.PistonQueueBukkit 5 | author: AlexProgrammerDE 6 | description: Bukkit companion for PistonQueue 7 | website: https://pistonmaster.net/ 8 | softdepend: [ "ProtocolLib" ] 9 | 10 | permissions: 11 | queue.admin: 12 | description: Lets you bypass the plugin. 13 | default: op 14 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle properties 2 | org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 3 | org.gradle.daemon=true 4 | org.gradle.parallel=true 5 | org.gradle.configureondemand=true 6 | org.gradle.caching=true 7 | org.gradle.configuration-cache=true 8 | org.gradle.vfs.watch=true 9 | org.gradle.console=rich 10 | org.gradle.warning.mode=all 11 | 12 | maven_version=3.1.4-SNAPSHOT 13 | -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("pq.java-conventions") 3 | } 4 | 5 | dependencies { 6 | api("de.exlll:configlib-yaml:4.7.0") 7 | compileOnly("net.pistonmaster:pistonmotd-api:5.2.7") 8 | compileOnly("org.apiguardian:apiguardian-api:1.1.2") 9 | api("com.github.spotbugs:spotbugs-annotations:4.9.8") 10 | compileOnly("com.google.guava:guava:33.5.0-jre") 11 | testImplementation("com.google.guava:guava:33.5.0-jre") 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 2 5 | indent_style = space 6 | insert_final_newline = true 7 | max_line_length = off 8 | tab_width = 2 9 | trim_trailing_whitespace = true 10 | ij_continuation_indent_size = 2 11 | 12 | [*.java] 13 | ij_java_keep_simple_blocks_in_one_line = false 14 | ij_java_keep_simple_classes_in_one_line = true 15 | ij_java_keep_simple_lambdas_in_one_line = true 16 | ij_java_keep_simple_methods_in_one_line = true 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled plugin files 2 | target/* 3 | 4 | # IDE files 5 | .idea/* 6 | .vscode/* 7 | .settings/* 8 | *.iml 9 | .classpath 10 | .project 11 | .factorypath 12 | 13 | # UML 14 | *.plantuml 15 | 16 | # Gradle 17 | .kotlin 18 | .gradle 19 | **/build/ 20 | **/run/ 21 | !src/**/build/ 22 | !src/**/run/ 23 | 24 | # Ignore Gradle GUI config 25 | gradle-app.setting 26 | 27 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 28 | !gradle-wrapper.jar 29 | 30 | # Cache of project 31 | .gradletasknamecache 32 | -------------------------------------------------------------------------------- /velocity/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("pq.platform-conventions") 3 | id("xyz.jpenilla.run-velocity") version "3.0.2" 4 | } 5 | 6 | dependencies { 7 | implementation(projects.pistonqueueShared) 8 | compileOnly(projects.pistonqueueBuildData) 9 | 10 | implementation("net.pistonmaster:PistonUtils:1.4.0") 11 | implementation("org.bstats:bstats-velocity:3.1.0") 12 | 13 | compileOnly("com.velocitypowered:velocity-api:3.1.1") 14 | compileOnly("com.github.spotbugs:spotbugs-annotations:4.9.8") 15 | } 16 | 17 | tasks { 18 | runVelocity { 19 | velocityVersion("3.4.0-SNAPSHOT") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /universal/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("pq.java-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":pistonqueue-bukkit", "shadow")) 7 | implementation(project(":pistonqueue-bungee", "shadow")) 8 | implementation(project(":pistonqueue-velocity", "shadow")) 9 | } 10 | 11 | tasks { 12 | jar { 13 | archiveClassifier.set("") 14 | archiveFileName.set("PistonQueue-${rootProject.version}.jar") 15 | destinationDirectory.set(rootProject.projectDir.resolve("build/libs")) 16 | 17 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 18 | dependsOn(configurations.runtimeClasspath) 19 | from({ configurations.runtimeClasspath.get().map { zipTree(it) } }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/chat/MessageType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.chat; 21 | 22 | public enum MessageType { 23 | CHAT, 24 | ACTION_BAR 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/chat/TextDecorationWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.chat; 21 | 22 | public enum TextDecorationWrapper { 23 | BOLD, 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/chat/TextColorWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.chat; 21 | 22 | public enum TextColorWrapper { 23 | GOLD, 24 | RED, 25 | DARK_BLUE, 26 | GREEN, 27 | } 28 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/chat/ComponentWrapperFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.chat; 21 | 22 | public interface ComponentWrapperFactory { 23 | ComponentWrapper text(String text); 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/wrapper/PermissibleWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.wrapper; 21 | 22 | public interface PermissibleWrapper { 23 | boolean hasPermission(String node); 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/set-version.yml: -------------------------------------------------------------------------------- 1 | name: set-version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version to set' 8 | required: true 9 | workflow_call: 10 | inputs: 11 | version: 12 | required: true 13 | type: string 14 | 15 | jobs: 16 | set-version: 17 | name: Set Version 18 | 19 | permissions: 20 | contents: write 21 | 22 | runs-on: ubuntu-24.04 23 | steps: 24 | - name: 'Shared: Checkout repository' 25 | uses: actions/checkout@v6 26 | with: 27 | ref: ${{ github.ref }} 28 | 29 | - name: 'Set Version' 30 | run: | 31 | sed -i 's/^maven_version=.*/maven_version='"${{ inputs.version }}"'/g' gradle.properties 32 | 33 | - name: 'Commit Version' 34 | uses: stefanzweifel/git-auto-commit-action@v7 35 | with: 36 | commit_message: 'chore(release): bump version to ${{ inputs.version }}' 37 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/events/PQPreLoginEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2022 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.events; 21 | 22 | public interface PQPreLoginEvent { 23 | boolean isCancelled(); 24 | 25 | void setCancelled(String reason); 26 | 27 | String getUsername(); 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload jar 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | # Only run on PRs if the source branch is on someone else's repo 8 | if: "${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }}" 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 14 | - name: Set up JDK 17 15 | uses: actions/setup-java@v5 16 | with: 17 | java-version: 17 18 | distribution: 'temurin' 19 | - name: Setup Gradle 20 | uses: gradle/actions/setup-gradle@v5 21 | - name: Build with Gradle 22 | run: ./gradlew build 23 | - name: Upload a Build Artifact 24 | uses: actions/upload-artifact@v6.0.0 25 | with: 26 | # Artifact name 27 | name: PistonQueue 28 | # A file, directory or wildcard pattern that describes what to upload 29 | path: build/libs/*.jar -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/wrapper/CommandSourceWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.wrapper; 21 | 22 | import net.pistonmaster.pistonqueue.shared.chat.ComponentWrapper; 23 | 24 | public interface CommandSourceWrapper extends PermissibleWrapper { 25 | void sendMessage(ComponentWrapper component); 26 | } 27 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/ShadowBanService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | /** 23 | * Abstraction around the static {@code StorageTool} API to make queue logic easier to test. 24 | */ 25 | public interface ShadowBanService { 26 | boolean isShadowBanned(String playerName); 27 | } 28 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/wrapper/ServerInfoWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.wrapper; 21 | 22 | import java.util.List; 23 | 24 | public interface ServerInfoWrapper { 25 | List getConnectedPlayers(); 26 | 27 | boolean isOnline(); 28 | 29 | void sendPluginMessage(String channel, byte[] data); 30 | } 31 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/chat/ComponentWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.chat; 21 | 22 | public interface ComponentWrapper { 23 | ComponentWrapper append(String text); 24 | 25 | ComponentWrapper append(ComponentWrapper component); 26 | 27 | ComponentWrapper color(TextColorWrapper color); 28 | 29 | ComponentWrapper decorate(TextDecorationWrapper decoration); 30 | } 31 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/BanType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue; 21 | 22 | /** 23 | * How shadow-banned people should be punished. 24 | */ 25 | public enum BanType { 26 | /** 27 | * Loop forever in queue! 28 | */ 29 | LOOP, 30 | 31 | /** 32 | * Have a custom chance of getting into the server! 33 | */ 34 | PERCENT, 35 | 36 | /** 37 | * Kick a player while joining! 38 | */ 39 | KICK 40 | } 41 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/events/PQServerConnectedEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.events; 21 | 22 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 23 | 24 | import java.util.Optional; 25 | 26 | /** 27 | * Event for when a player has successfully moved to another server on the proxy. 28 | */ 29 | public interface PQServerConnectedEvent { 30 | PlayerWrapper getPlayer(); 31 | 32 | Optional getPreviousServer(); 33 | 34 | String getServer(); 35 | } 36 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/StorageShadowBanService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.utils.StorageTool; 23 | 24 | /** 25 | * Production implementation that defers to {@link StorageTool}. 26 | */ 27 | public final class StorageShadowBanService implements ShadowBanService { 28 | @Override 29 | public boolean isShadowBanned(String playerName) { 30 | return StorageTool.isShadowBanned(playerName); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/events/PQKickedFromServerEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.events; 21 | 22 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 23 | 24 | import java.util.Optional; 25 | 26 | public interface PQKickedFromServerEvent { 27 | void setCancelServer(String server); 28 | 29 | void setKickMessage(String message); 30 | 31 | PlayerWrapper getPlayer(); 32 | 33 | String getKickedFrom(); 34 | 35 | Optional getKickReason(); 36 | 37 | boolean willDisconnect(); 38 | } 39 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/events/PQServerPreConnectEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.events; 21 | 22 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 23 | 24 | import java.util.Optional; 25 | 26 | /** 27 | * Event for trying to connect to a server that allows us to intercept the connection and redirect the player. 28 | */ 29 | public interface PQServerPreConnectEvent { 30 | PlayerWrapper getPlayer(); 31 | 32 | Optional getTarget(); 33 | 34 | void setTarget(String server); 35 | } 36 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/wrapper/PlayerWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.wrapper; 21 | 22 | import net.pistonmaster.pistonqueue.shared.chat.MessageType; 23 | 24 | import java.util.List; 25 | import java.util.Optional; 26 | import java.util.UUID; 27 | 28 | public interface PlayerWrapper extends PermissibleWrapper { 29 | void connect(String server); 30 | 31 | Optional getCurrentServer(); 32 | 33 | default void sendMessage(String message) { 34 | sendMessage(MessageType.CHAT, message); 35 | } 36 | 37 | void sendMessage(MessageType type, String message); 38 | 39 | void sendPlayerList(List header, List footer); 40 | 41 | void resetPlayerList(); 42 | 43 | String getName(); 44 | 45 | UUID getUniqueId(); 46 | 47 | void disconnect(String message); 48 | } 49 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/config/StorageData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.config; 21 | 22 | import de.exlll.configlib.Comment; 23 | import de.exlll.configlib.Configuration; 24 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 25 | 26 | import java.util.Collections; 27 | import java.util.LinkedHashMap; 28 | import java.util.Map; 29 | 30 | @Configuration 31 | public final class StorageData { 32 | @Comment({ 33 | "Shadow banned players mapped to the date when they should be unbanned." 34 | }) 35 | private Map bans = new LinkedHashMap<>(); 36 | 37 | public Map getBans() { 38 | return Collections.unmodifiableMap(bans); 39 | } 40 | 41 | @SuppressFBWarnings( 42 | value = "EI_EXPOSE_REP", 43 | justification = "Mutable map required for runtime updates" 44 | ) 45 | public Map getMutableBans() { 46 | return bans; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | plugins { 10 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 11 | } 12 | 13 | dependencyResolutionManagement { 14 | @Suppress("UnstableApiUsage") 15 | repositories { 16 | mavenCentral() 17 | maven("https://central.sonatype.com/repository/maven-snapshots/") { 18 | name = "Sonatype Snapshot Repository" 19 | mavenContent { snapshotsOnly() } 20 | } 21 | maven("https://papermc.io/repo/repository/maven-public/") { 22 | name = "PaperMC" 23 | } 24 | maven("https://nexus.velocitypowered.com/repository/maven-public/") { 25 | name = "VelocityPowered" 26 | } 27 | maven("https://repo.codemc.org/repository/maven-public") { 28 | name = "CodeMC" 29 | } 30 | maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") { 31 | name = "PlaceholderAPI" 32 | } 33 | } 34 | } 35 | 36 | rootProject.name = "PistonQueue" 37 | 38 | setOf( 39 | "build-data", 40 | "placeholder", 41 | "shared", 42 | "bukkit", 43 | "bungee", 44 | "velocity", 45 | "universal" 46 | ).forEach { setupPQSubproject(it) } 47 | 48 | fun setupPQSubproject(name: String) { 49 | setupSubproject("pistonqueue-$name") { 50 | projectDir = file(name) 51 | } 52 | } 53 | 54 | inline fun setupSubproject(name: String, block: ProjectDescriptor.() -> Unit) { 55 | include(name) 56 | project(":$name").apply(block) 57 | } 58 | -------------------------------------------------------------------------------- /placeholder/src/main/java/net/pistonmaster/pistonqueue/placeholder/PAPIExpansion.java: -------------------------------------------------------------------------------- 1 | package net.pistonmaster.pistonqueue.placeholder; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.clip.placeholderapi.expansion.PlaceholderExpansion; 5 | import org.bukkit.OfflinePlayer; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.Map; 9 | import java.util.Objects; 10 | 11 | @RequiredArgsConstructor 12 | public final class PAPIExpansion extends PlaceholderExpansion { 13 | private final PistonQueuePlaceholder plugin; 14 | 15 | @Override 16 | public boolean canRegister() { 17 | return true; 18 | } 19 | 20 | @Override 21 | public @NotNull String getAuthor() { 22 | return "AlexProgrammerDE"; 23 | } 24 | 25 | @Override 26 | public @NotNull String getIdentifier() { 27 | return "pistonqueue"; 28 | } 29 | 30 | @Override 31 | public @NotNull String getVersion() { 32 | return plugin.getDescription().getVersion(); 33 | } 34 | 35 | @Override 36 | public String onRequest(OfflinePlayer player, @NotNull String identifier) { 37 | for (Map.Entry entry : plugin.getOnlineQueue().entrySet()) { 38 | if (identifier.equalsIgnoreCase("online_queue_" + entry.getKey())) { 39 | return String.valueOf(entry.getValue()); 40 | } 41 | } 42 | 43 | for (Map.Entry entry : plugin.getOnlineTarget().entrySet()) { 44 | if (identifier.equalsIgnoreCase("online_target_" + entry.getKey())) { 45 | return String.valueOf(entry.getValue()); 46 | } 47 | } 48 | 49 | return null; 50 | } 51 | 52 | @Override 53 | public int hashCode() { 54 | return Objects.hash(getIdentifier()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![modrinth](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/modrinth_vector.svg)](https://modrinth.com/plugin/pistonqueue) 4 | 5 | [![discord](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/discord-singular_vector.svg)](https://discord.gg/J9bmJNuTJm) [![kofi](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/kofi-singular_vector.svg)](https://ko-fi.com/alexprogrammerde) 6 | 7 | # PistonQueue 8 | 9 | **⏰️ Powerful queue plugin for anarchy/survival servers.** 10 | 11 | ## About 12 | 13 | PistonQueue is a powerful, but easy to use queue plugin designed for anarchy and survival servers. 14 | It is a recreation of the 2b2t.org queue system design, but adds a lot of features on top. 15 | 16 | ## Features 17 | 18 | * BungeeCord and Velocity support. 19 | * Queue system with reserved slots. 20 | * Shadow-banning players. 21 | * Built-in support for forcing people into the end void. 22 | * Auth server support for cracked (offline mode: false) servers. 23 | * Joining the auth server first before the queue server. 24 | 25 | ## Setup 26 | 27 | Check out the [wiki](https://github.com/AlexProgrammerDE/PistonQueue/wiki) for a tutorial on how to set up PistonQueue. 28 | 29 | ## 🌈 Community 30 | 31 | Feel free to join our Discord community server: 32 | 33 | [![Discord Banner](https://discord.com/api/guilds/739784741124833301/widget.png?style=banner2)](https://discord.gg/J9bmJNuTJm) 34 | 35 | This project is in active development, so if you have any feature requests or issues please submit them here on GitHub. PRs are welcome, too. :octocat: 36 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/UsernameValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.events.PQPreLoginEvent; 24 | 25 | import java.util.Objects; 26 | 27 | /** 28 | * Handles username validation for pre-login events. 29 | */ 30 | public final class UsernameValidator { 31 | private final Config config; 32 | 33 | public UsernameValidator(Config config) { 34 | this.config = Objects.requireNonNull(config, "config"); 35 | } 36 | 37 | /** 38 | * Validates the username in the pre-login event and cancels it if it doesn't match the regex. 39 | * 40 | * @param event the pre-login event 41 | */ 42 | public void validateUsername(PQPreLoginEvent event) { 43 | if (event.isCancelled()) { 44 | return; 45 | } 46 | 47 | if (config.enableUsernameRegex() && !event.getUsername().matches(config.usernameRegex())) { 48 | event.setCancelled(config.usernameRegexMessage().replace("%regex%", config.usernameRegex())); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /bungee/src/main/java/net/pistonmaster/pistonqueue/bungee/utils/ChatUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.bungee.utils; 21 | 22 | import net.md_5.bungee.api.ChatColor; 23 | import net.md_5.bungee.api.chat.BaseComponent; 24 | import net.md_5.bungee.api.chat.TextComponent; 25 | import net.pistonmaster.pistonqueue.shared.config.Config; 26 | import net.pistonmaster.pistonqueue.shared.utils.SharedChatUtils; 27 | 28 | import java.util.List; 29 | import java.util.stream.Collectors; 30 | 31 | public final class ChatUtils { 32 | private ChatUtils() { 33 | } 34 | 35 | public static String parseToString(Config config, String str) { 36 | return ChatColor.translateAlternateColorCodes('&', SharedChatUtils.parseText(config, str)); 37 | } 38 | 39 | public static BaseComponent parseToComponent(Config config, String str) { 40 | return TextComponent.fromLegacy(parseToString(config, str)); 41 | } 42 | 43 | public static BaseComponent parseTab(Config config, List tab) { 44 | return parseToComponent(config, tab.stream() 45 | .map(line -> parseToString(config, line)) 46 | .collect(Collectors.joining("\n"))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueAvailabilityCalculator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 23 | 24 | import java.util.concurrent.locks.Lock; 25 | 26 | /** 27 | * Encapsulates the slot and fullness calculation so it can be reused and 28 | * tested independently from the listener implementation. 29 | */ 30 | public final class QueueAvailabilityCalculator { 31 | 32 | public boolean isServerFull(QueueType type) { 33 | return isTargetFull(type) || hasQueuedPlayers(type); 34 | } 35 | 36 | public boolean isTargetFull(QueueType type) { 37 | return getFreeSlots(type) <= 0; 38 | } 39 | 40 | public int getFreeSlots(QueueType type) { 41 | return type.getReservedSlots() - type.getPlayersWithTypeInTarget().get(); 42 | } 43 | 44 | private boolean hasQueuedPlayers(QueueType type) { 45 | Lock readLock = type.getQueueLock().readLock(); 46 | readLock.lock(); 47 | try { 48 | return !type.getQueueMap().isEmpty(); 49 | } finally { 50 | readLock.unlock(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/ShadowBanKickHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.BanType; 24 | import net.pistonmaster.pistonqueue.shared.utils.StorageTool; 25 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 26 | 27 | import java.util.Objects; 28 | 29 | /** 30 | * Handles shadow ban kicks for players after they log in. 31 | */ 32 | public final class ShadowBanKickHandler { 33 | private final Config config; 34 | 35 | public ShadowBanKickHandler(Config config) { 36 | this.config = Objects.requireNonNull(config, "config"); 37 | } 38 | 39 | /** 40 | * Checks if the player should be kicked due to shadow ban and disconnects them if so. 41 | * 42 | * @param player the player who just logged in 43 | */ 44 | public void handleShadowBanKick(PlayerWrapper player) { 45 | if (StorageTool.isShadowBanned(player.getName()) && config.shadowBanType() == BanType.KICK) { 46 | player.disconnect(config.serverDownKickMessage()); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /velocity/src/main/java/net/pistonmaster/pistonqueue/velocity/utils/ChatUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.velocity.utils; 21 | 22 | import net.kyori.adventure.text.TextComponent; 23 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 24 | import net.pistonmaster.pistonqueue.shared.config.Config; 25 | import net.pistonmaster.pistonqueue.shared.utils.SharedChatUtils; 26 | 27 | import java.util.List; 28 | import java.util.stream.Collectors; 29 | 30 | public final class ChatUtils { 31 | private ChatUtils() { 32 | } 33 | 34 | public static TextComponent parseToComponent(Config config, String str) { 35 | return LegacyComponentSerializer.legacySection().deserialize(parseToString(config, str)); 36 | } 37 | 38 | public static String parseToString(Config config, String str) { 39 | return LegacyComponentSerializer.legacySection().serialize(LegacyComponentSerializer.legacyAmpersand().deserialize(SharedChatUtils.parseText(config, str))); 40 | } 41 | 42 | public static TextComponent parseTab(Config config, List tab) { 43 | return parseToComponent(config, 44 | tab.stream() 45 | .map(line -> parseToString(config, line)) 46 | .collect(Collectors.joining("\n")) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /bukkit/src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # PistonQueue V${version} 2 | # Note - Operators and users with the permission "queue.admin" will be excluded from restrictions. 3 | 4 | location: 5 | enabled: true # Force the user to remain in a certain position. 6 | world: "world_the_end" # Forced world 7 | coordinates: 8 | x: 500 # Forced x coordinate 9 | y: 256 # Forced y coordinate 10 | z: 500 # Forced z coordinate 11 | 12 | visibility: 13 | hidePlayers: true # Hide players from each other, so that it looks like every user is alone in the world. This will also disable join/leave messages. 14 | restrictMovement: true # Prevent players from moving. 15 | forceGamemode: 16 | enabled: true # Force players to remain in a gamemode. 17 | mode: "spectator" # The gamemode to force players to remain in. 18 | team: 19 | enabled: false # Show a player's own name to themselves in spectator menu. 20 | name: "%player_name%" # The team name the user sees. (Valid placeholders: %player_name%, %random%) 21 | 22 | communication: 23 | disableChat: true # Don't allow players to chat. 24 | disableCommands: true # Don't allow commands. 25 | 26 | audio: 27 | playXpSound: true # Plays an XP sound when the proxy sends a plugin message. 28 | 29 | protections: 30 | preventExperience: true # Prevents players from gaining experience. 31 | preventDamage: true # Prevents players from getting damage. 32 | preventHunger: true # Prevents players from gaining hunger. 33 | 34 | # ProtocolLib only 35 | protocolLib: 36 | disableDebug: true # Doesn't show the client's position on F3. 37 | suppressPackets: 38 | chunk: true # Do not send chunk data packets. 39 | time: true # Do not send time packets. 40 | health: true # Do not send health packets. 41 | advancement: true # Do not send advancement packets. 42 | experience: true # Do not send experience packets. 43 | showFullHead: true # Does not send entity metadata anymore, causing that the entire player head is shown while in spectator. 44 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/config/ConfigMigrator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.config; 21 | 22 | import de.exlll.configlib.NameFormatters; 23 | import de.exlll.configlib.YamlConfigurations; 24 | 25 | import java.io.IOException; 26 | import java.nio.charset.StandardCharsets; 27 | import java.nio.file.Files; 28 | import java.nio.file.Path; 29 | 30 | /** 31 | * Performs migrations of legacy configuration files to the new format. 32 | */ 33 | public final class ConfigMigrator { 34 | private ConfigMigrator() { 35 | } 36 | 37 | public static void migrate(Path file) throws IOException { 38 | if (!Files.exists(file)) { 39 | return; 40 | } 41 | 42 | String content = Files.readString(file, StandardCharsets.UTF_8); 43 | if (content.contains("configVersion:")) { 44 | return; 45 | } 46 | 47 | Config.ConfigLegacyV1 legacy = YamlConfigurations.load( 48 | file, 49 | Config.ConfigLegacyV1.class, 50 | builder -> builder.setNameFormatter(NameFormatters.IDENTITY) 51 | ); 52 | 53 | Config migrated = Config.fromLegacy(legacy); 54 | YamlConfigurations.save( 55 | file, 56 | Config.class, 57 | migrated, 58 | builder -> builder.setNameFormatter(NameFormatters.IDENTITY) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/QueueGroup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Collections; 24 | import java.util.List; 25 | 26 | public final class QueueGroup { 27 | private final String name; 28 | private final String queueServer; 29 | private final List targetServers; 30 | private final List sourceServers; 31 | private final QueueType[] queueTypes; 32 | 33 | public QueueGroup(String name, String queueServer, List targetServers, List sourceServers, QueueType[] queueTypes) { 34 | this.name = name; 35 | this.queueServer = queueServer; 36 | this.targetServers = Collections.unmodifiableList(new ArrayList<>(targetServers)); 37 | this.sourceServers = Collections.unmodifiableList(new ArrayList<>(sourceServers)); 38 | this.queueTypes = queueTypes == null ? new QueueType[0] : queueTypes.clone(); 39 | } 40 | 41 | public String getName() { 42 | return name; 43 | } 44 | 45 | public String getQueueServer() { 46 | return queueServer; 47 | } 48 | 49 | public List getTargetServers() { 50 | return targetServers; 51 | } 52 | 53 | public List getSourceServers() { 54 | return sourceServers; 55 | } 56 | 57 | public QueueType[] getQueueTypes() { 58 | return queueTypes.clone(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/hooks/PistonMOTDPlaceholder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.hooks; 21 | 22 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 23 | import net.pistonmaster.pistonmotd.api.PlaceholderParser; 24 | import net.pistonmaster.pistonmotd.api.PlaceholderUtil; 25 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 26 | import net.pistonmaster.pistonqueue.shared.config.Config; 27 | 28 | import java.util.Locale; 29 | import java.util.concurrent.locks.Lock; 30 | 31 | public final class PistonMOTDPlaceholder implements PlaceholderParser { 32 | private final Config config; 33 | 34 | @SuppressFBWarnings( 35 | value = "EI_EXPOSE_REP2", 36 | justification = "Placeholder must reflect live configuration changes and only reads from the provided reference" 37 | ) 38 | public PistonMOTDPlaceholder(Config config) { 39 | this.config = config; 40 | PlaceholderUtil.registerParser(this); 41 | } 42 | 43 | @Override 44 | public String parseString(String s) { 45 | for (QueueType type : config.getAllQueueTypes()) { 46 | s = s.replace("%pistonqueue_" + type.getName().toLowerCase(Locale.ROOT) + "%", String.valueOf(queueSize(type))); 47 | } 48 | return s; 49 | } 50 | 51 | private static int queueSize(QueueType type) { 52 | Lock readLock = type.getQueueLock().readLock(); 53 | readLock.lock(); 54 | try { 55 | return type.getQueueMap().size(); 56 | } finally { 57 | readLock.unlock(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/utils/SharedChatUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.utils; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 24 | 25 | import java.time.Duration; 26 | import java.util.Locale; 27 | import java.util.concurrent.locks.Lock; 28 | 29 | public final class SharedChatUtils { 30 | private SharedChatUtils() { 31 | } 32 | 33 | public static String formatDuration(String str, Duration duration, int position) { 34 | String format = duration.toHours() == 0 35 | ? "%dm".formatted(duration.toMinutes() == 0 ? 1 : duration.toMinutes()) 36 | : "%dh %dm".formatted(duration.toHours(), duration.toMinutes() % 60); 37 | 38 | return str.replace("%position%", String.valueOf(position)).replace("%wait%", format); 39 | } 40 | 41 | public static String parseText(Config config, String text) { 42 | text = text.replace("%server_name%", config.serverName()); 43 | for (QueueType type : config.getAllQueueTypes()) { 44 | text = text.replace("%" + type.getName().toLowerCase(Locale.ROOT) + "%", String.valueOf(queueSize(type))); 45 | } 46 | text = text.replace("%position%", "None"); 47 | text = text.replace("%wait%", "None"); 48 | 49 | return text; 50 | } 51 | 52 | private static int queueSize(QueueType type) { 53 | Lock readLock = type.getQueueLock().readLock(); 54 | readLock.lock(); 55 | try { 56 | return type.getQueueMap().size(); 57 | } finally { 58 | readLock.unlock(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /bukkit/src/main/java/net/pistonmaster/pistonqueue/bukkit/QueuePluginMessageListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.bukkit; 21 | 22 | import com.google.common.io.ByteArrayDataInput; 23 | import com.google.common.io.ByteStreams; 24 | import lombok.RequiredArgsConstructor; 25 | import org.bukkit.Sound; 26 | import org.bukkit.entity.Player; 27 | import org.bukkit.plugin.messaging.PluginMessageListener; 28 | import org.jetbrains.annotations.NotNull; 29 | 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | import java.util.UUID; 33 | 34 | @RequiredArgsConstructor 35 | public final class QueuePluginMessageListener implements PluginMessageListener { 36 | private final PistonQueueBukkit plugin; 37 | 38 | @Override 39 | @SuppressWarnings("UnstableApiUsage") 40 | public void onPluginMessageReceived(@NotNull String channel, @NotNull Player messagePlayer, byte[] message) { 41 | if (!"piston:queue".equals(channel)) { 42 | return; 43 | } 44 | 45 | ByteArrayDataInput in = ByteStreams.newDataInput(message); 46 | String subChannel = in.readUTF(); 47 | 48 | if (plugin.isPlayXP() && "xpV2".equals(subChannel)) { 49 | List uuids = new ArrayList<>(); 50 | int count = in.readInt(); 51 | for (int i = 0; i < count; i++) { 52 | uuids.add(UUID.fromString(in.readUTF())); 53 | } 54 | 55 | for (UUID uuid : uuids) { 56 | Player target = plugin.getServer().getPlayer(uuid); 57 | 58 | if (target == null) { 59 | continue; 60 | } 61 | 62 | target.playSound(target.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 100.0F, 1.0F); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/QueueType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue; 21 | 22 | import lombok.AllArgsConstructor; 23 | import lombok.Getter; 24 | import lombok.Setter; 25 | 26 | import java.time.Duration; 27 | import java.time.Instant; 28 | import java.util.LinkedHashMap; 29 | import java.util.List; 30 | import java.util.Map; 31 | import java.util.Set; 32 | import java.util.UUID; 33 | import java.util.concurrent.ConcurrentHashMap; 34 | import java.util.concurrent.atomic.AtomicInteger; 35 | import java.util.concurrent.locks.ReadWriteLock; 36 | import java.util.concurrent.locks.ReentrantReadWriteLock; 37 | 38 | @Getter 39 | @AllArgsConstructor 40 | public class QueueType { 41 | private final Map queueMap = new LinkedHashMap<>(); 42 | private final Map durationFromPosition = new LinkedHashMap<>(); 43 | private final ReadWriteLock queueLock = new ReentrantReadWriteLock(); 44 | private final ReadWriteLock durationLock = new ReentrantReadWriteLock(); 45 | private final Map> positionCache = new ConcurrentHashMap<>(); 46 | private final Set activeTransfers = ConcurrentHashMap.newKeySet(); 47 | private final AtomicInteger playersWithTypeInTarget = new AtomicInteger(); 48 | private final String name; 49 | @Setter 50 | private volatile int order; 51 | @Setter 52 | private String permission; 53 | @Setter 54 | private volatile int reservedSlots; 55 | @Setter 56 | private List header; 57 | @Setter 58 | private List footer; 59 | 60 | public enum QueueReason { 61 | SERVER_FULL, 62 | SERVER_DOWN, 63 | RECOVERY 64 | } 65 | 66 | public record QueuedPlayer( 67 | String targetServer, 68 | QueueReason queueReason 69 | ) {} 70 | } 71 | -------------------------------------------------------------------------------- /bungee/src/main/java/net/pistonmaster/pistonqueue/bungee/commands/MainCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.bungee.commands; 21 | 22 | import net.md_5.bungee.api.CommandSender; 23 | import net.md_5.bungee.api.chat.ComponentBuilder; 24 | import net.md_5.bungee.api.plugin.Command; 25 | import net.md_5.bungee.api.plugin.TabExecutor; 26 | import net.pistonmaster.pistonqueue.bungee.PistonQueueBungee; 27 | import net.pistonmaster.pistonqueue.shared.chat.ComponentWrapper; 28 | import net.pistonmaster.pistonqueue.shared.chat.ComponentWrapperFactory; 29 | import net.pistonmaster.pistonqueue.shared.command.MainCommandShared; 30 | import net.pistonmaster.pistonqueue.shared.wrapper.CommandSourceWrapper; 31 | 32 | public final class MainCommand extends Command implements TabExecutor, MainCommandShared { 33 | private final PistonQueueBungee plugin; 34 | 35 | public MainCommand(PistonQueueBungee plugin) { 36 | super("pistonqueue", null, "pq"); 37 | this.plugin = plugin; 38 | } 39 | 40 | @Override 41 | public void execute(CommandSender sender, String[] args) { 42 | onCommand(new CommandSourceWrapper() { 43 | @Override 44 | public void sendMessage(ComponentWrapper component) { 45 | sender.sendMessage(((BungeeComponentWrapperImpl) component).toBaseComponents()); 46 | } 47 | 48 | @Override 49 | public boolean hasPermission(String node) { 50 | return sender.hasPermission(node); 51 | } 52 | }, args, plugin); 53 | } 54 | 55 | @Override 56 | public Iterable onTabComplete(CommandSender sender, String[] args) { 57 | return onTab(args, sender::hasPermission, plugin); 58 | } 59 | 60 | @Override 61 | public ComponentWrapperFactory component() { 62 | return text -> new BungeeComponentWrapperImpl(new ComponentBuilder(text)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /velocity/src/main/java/net/pistonmaster/pistonqueue/velocity/commands/MainCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.velocity.commands; 21 | 22 | import com.velocitypowered.api.command.CommandSource; 23 | import com.velocitypowered.api.command.SimpleCommand; 24 | import lombok.RequiredArgsConstructor; 25 | import net.kyori.adventure.text.Component; 26 | import net.pistonmaster.pistonqueue.shared.chat.ComponentWrapper; 27 | import net.pistonmaster.pistonqueue.shared.chat.ComponentWrapperFactory; 28 | import net.pistonmaster.pistonqueue.shared.command.MainCommandShared; 29 | import net.pistonmaster.pistonqueue.shared.wrapper.CommandSourceWrapper; 30 | import net.pistonmaster.pistonqueue.velocity.PistonQueueVelocity; 31 | 32 | import java.util.List; 33 | 34 | @RequiredArgsConstructor 35 | public final class MainCommand implements SimpleCommand, MainCommandShared { 36 | private final PistonQueueVelocity plugin; 37 | 38 | @Override 39 | public void execute(Invocation invocation) { 40 | String[] args = invocation.arguments(); 41 | CommandSource sender = invocation.source(); 42 | 43 | onCommand(new CommandSourceWrapper() { 44 | @Override 45 | public void sendMessage(ComponentWrapper component) { 46 | sender.sendMessage(((VelocityComponentWrapperImpl) component).mainComponent()); 47 | } 48 | 49 | @Override 50 | public boolean hasPermission(String node) { 51 | return sender.hasPermission(node); 52 | } 53 | }, args, plugin); 54 | } 55 | 56 | @Override 57 | public List suggest(Invocation invocation) { 58 | return onTab(invocation.arguments(), invocation.source()::hasPermission, plugin); 59 | } 60 | 61 | @Override 62 | public ComponentWrapperFactory component() { 63 | return text -> new VelocityComponentWrapperImpl(Component.text(text)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueMoveProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 25 | 26 | import java.util.Objects; 27 | 28 | /** 29 | * Coordinates moving the queue forward by delegating to specialized helpers. 30 | */ 31 | public final class QueueMoveProcessor { 32 | private final QueueEnvironment environment; 33 | private final QueueCleaner queueCleaner; 34 | private final QueueRecoveryHandler recoveryHandler; 35 | private final QueueConnector queueConnector; 36 | 37 | public QueueMoveProcessor( 38 | QueueEnvironment environment, 39 | QueueCleaner queueCleaner, 40 | QueueRecoveryHandler recoveryHandler, 41 | QueueConnector queueConnector 42 | ) { 43 | this.environment = Objects.requireNonNull(environment, "environment"); 44 | this.queueCleaner = Objects.requireNonNull(queueCleaner, "queueCleaner"); 45 | this.recoveryHandler = Objects.requireNonNull(recoveryHandler, "recoveryHandler"); 46 | this.queueConnector = Objects.requireNonNull(queueConnector, "queueConnector"); 47 | } 48 | 49 | public void processQueues() { 50 | Config config = environment.config(); 51 | for (QueueGroup group : config.getQueueGroups()) { 52 | queueCleaner.cleanGroup(group); 53 | } 54 | 55 | if (config.recovery()) { 56 | environment.plugin().getPlayers().forEach(recoveryHandler::recoverPlayer); 57 | } 58 | 59 | for (QueueGroup group : config.getQueueGroups()) { 60 | if (config.pauseQueueIfTargetDown() && !environment.isGroupTargetOnline(group)) { 61 | continue; 62 | } 63 | for (QueueType type : group.getQueueTypes()) { 64 | queueConnector.connectPlayers(group, type); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueRecoveryHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 25 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 26 | 27 | import java.util.Objects; 28 | import java.util.Optional; 29 | import java.util.concurrent.locks.Lock; 30 | 31 | /** 32 | * Puts players back into the queue when recovery is enabled and something went wrong with their connection. 33 | */ 34 | public final class QueueRecoveryHandler { 35 | private final QueueEnvironment environment; 36 | 37 | public QueueRecoveryHandler(QueueEnvironment environment) { 38 | this.environment = Objects.requireNonNull(environment, "environment"); 39 | } 40 | 41 | public void recoverPlayer(PlayerWrapper player) { 42 | Config config = environment.config(); 43 | QueueType type = config.getQueueType(player); 44 | QueueGroup group = environment.resolveGroupForType(type); 45 | 46 | Optional currentServer = player.getCurrentServer(); 47 | if (currentServer.isPresent() 48 | && currentServer.get().equals(group.getQueueServer()) 49 | && !type.getActiveTransfers().contains(player.getUniqueId())) { 50 | boolean addedToQueue = false; 51 | Lock writeLock = type.getQueueLock().writeLock(); 52 | writeLock.lock(); 53 | try { 54 | if (!type.getQueueMap().containsKey(player.getUniqueId())) { 55 | type.getQueueMap().put(player.getUniqueId(), new QueueType.QueuedPlayer(environment.defaultTarget(group), QueueType.QueueReason.RECOVERY)); 56 | addedToQueue = true; 57 | } 58 | } finally { 59 | writeLock.unlock(); 60 | } 61 | 62 | if (addedToQueue) { 63 | player.sendMessage(config.recoveryMessage()); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /velocity/src/main/java/net/pistonmaster/pistonqueue/velocity/commands/VelocityComponentWrapperImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.velocity.commands; 21 | 22 | import net.kyori.adventure.text.Component; 23 | import net.kyori.adventure.text.format.NamedTextColor; 24 | import net.kyori.adventure.text.format.TextDecoration; 25 | import net.pistonmaster.pistonqueue.shared.chat.ComponentWrapper; 26 | import net.pistonmaster.pistonqueue.shared.chat.TextColorWrapper; 27 | import net.pistonmaster.pistonqueue.shared.chat.TextDecorationWrapper; 28 | 29 | public final class VelocityComponentWrapperImpl implements ComponentWrapper { 30 | private final Component mainComponent; 31 | 32 | public VelocityComponentWrapperImpl(Component mainComponent) { 33 | this.mainComponent = copyComponent(mainComponent); 34 | } 35 | 36 | Component mainComponent() { 37 | return mainComponent; 38 | } 39 | 40 | private static Component copyComponent(Component component) { 41 | if (component == null) { 42 | return Component.empty(); 43 | } 44 | return Component.empty().append(component); 45 | } 46 | 47 | @Override 48 | public ComponentWrapper append(String text) { 49 | return new VelocityComponentWrapperImpl(mainComponent.append(Component.text(text))); 50 | } 51 | 52 | @Override 53 | public ComponentWrapper append(ComponentWrapper component) { 54 | return new VelocityComponentWrapperImpl(mainComponent.append(((VelocityComponentWrapperImpl) component).mainComponent)); 55 | } 56 | 57 | @Override 58 | public ComponentWrapper color(TextColorWrapper color) { 59 | return new VelocityComponentWrapperImpl(mainComponent.color(switch (color) { 60 | case GOLD -> NamedTextColor.GOLD; 61 | case RED -> NamedTextColor.RED; 62 | case DARK_BLUE -> NamedTextColor.DARK_BLUE; 63 | case GREEN -> NamedTextColor.GREEN; 64 | })); 65 | } 66 | 67 | @Override 68 | public ComponentWrapper decorate(TextDecorationWrapper decoration) { 69 | return new VelocityComponentWrapperImpl(mainComponent.decorate(switch (decoration) { 70 | case BOLD -> TextDecoration.BOLD; 71 | })); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueEntryFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.events.PQServerPreConnectEvent; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 25 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 26 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 27 | 28 | import java.util.Map; 29 | import java.util.Optional; 30 | import java.util.UUID; 31 | import java.util.concurrent.locks.Lock; 32 | 33 | /** 34 | * Handles the bookkeeping required when a player gets placed into a queue. 35 | */ 36 | public final class QueueEntryFactory { 37 | private final QueueEnvironment environment; 38 | 39 | public QueueEntryFactory(QueueEnvironment environment) { 40 | this.environment = environment; 41 | } 42 | 43 | public void enqueue(PlayerWrapper player, QueueGroup group, QueueType type, PQServerPreConnectEvent event, boolean serverFull, Config config) { 44 | player.sendPlayerList(type.getHeader(), type.getFooter()); 45 | 46 | Optional originalTarget = event.getTarget(); 47 | event.setTarget(group.getQueueServer()); 48 | 49 | Map queueMap = type.getQueueMap(); 50 | String queueTarget; 51 | if (config.forceTargetServer() || originalTarget.isEmpty()) { 52 | queueTarget = environment.defaultTarget(group); 53 | } else { 54 | queueTarget = originalTarget.get(); 55 | } 56 | 57 | UUID playerId = player.getUniqueId(); 58 | boolean shouldNotifyFull = false; 59 | Lock writeLock = type.getQueueLock().writeLock(); 60 | writeLock.lock(); 61 | try { 62 | if (serverFull && !queueMap.containsKey(playerId)) { 63 | shouldNotifyFull = true; 64 | } 65 | 66 | queueMap.putIfAbsent(playerId, new QueueType.QueuedPlayer(queueTarget, QueueType.QueueReason.SERVER_FULL)); 67 | } finally { 68 | writeLock.unlock(); 69 | } 70 | 71 | if (shouldNotifyFull) { 72 | player.sendMessage(config.serverIsFullMessage()); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueCleaner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 23 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 24 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 25 | 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Objects; 30 | import java.util.Optional; 31 | import java.util.UUID; 32 | import java.util.concurrent.locks.Lock; 33 | 34 | /** 35 | * Removes stale entries from the queue maps to prevent memory leaks. 36 | */ 37 | public final class QueueCleaner { 38 | private final QueueEnvironment environment; 39 | 40 | public QueueCleaner(QueueEnvironment environment) { 41 | this.environment = Objects.requireNonNull(environment, "environment"); 42 | } 43 | 44 | public void cleanGroup(QueueGroup group) { 45 | for (QueueType type : group.getQueueTypes()) { 46 | Map queueMap = type.getQueueMap(); 47 | List queueSnapshot; 48 | Lock readLock = type.getQueueLock().readLock(); 49 | readLock.lock(); 50 | try { 51 | queueSnapshot = new ArrayList<>(queueMap.keySet()); 52 | } finally { 53 | readLock.unlock(); 54 | } 55 | 56 | if (queueSnapshot.isEmpty()) { 57 | continue; 58 | } 59 | 60 | List staleEntries = new ArrayList<>(); 61 | for (UUID uuid : queueSnapshot) { 62 | Optional player = environment.plugin().getPlayer(uuid); 63 | Optional optionalTarget = player.flatMap(PlayerWrapper::getCurrentServer); 64 | if (optionalTarget.isEmpty() || !optionalTarget.get().equals(group.getQueueServer())) { 65 | staleEntries.add(uuid); 66 | } 67 | } 68 | 69 | if (!staleEntries.isEmpty()) { 70 | Lock writeLock = type.getQueueLock().writeLock(); 71 | writeLock.lock(); 72 | try { 73 | staleEntries.forEach(queueMap::remove); 74 | } finally { 75 | writeLock.unlock(); 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bungee/src/main/java/net/pistonmaster/pistonqueue/bungee/commands/BungeeComponentWrapperImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.bungee.commands; 21 | 22 | import net.md_5.bungee.api.ChatColor; 23 | import net.md_5.bungee.api.chat.BaseComponent; 24 | import net.md_5.bungee.api.chat.ComponentBuilder; 25 | import net.pistonmaster.pistonqueue.shared.chat.ComponentWrapper; 26 | import net.pistonmaster.pistonqueue.shared.chat.TextColorWrapper; 27 | import net.pistonmaster.pistonqueue.shared.chat.TextDecorationWrapper; 28 | 29 | final class BungeeComponentWrapperImpl implements ComponentWrapper { 30 | private final ComponentBuilder componentBuilder; 31 | 32 | BungeeComponentWrapperImpl(ComponentBuilder componentBuilder) { 33 | this.componentBuilder = copyBuilder(componentBuilder); 34 | } 35 | 36 | private BungeeComponentWrapperImpl(ComponentBuilder componentBuilder, boolean trusted) { 37 | this.componentBuilder = trusted ? componentBuilder : copyBuilder(componentBuilder); 38 | } 39 | 40 | private static ComponentBuilder copyBuilder(ComponentBuilder source) { 41 | return new ComponentBuilder(source); 42 | } 43 | 44 | BaseComponent[] toBaseComponents() { 45 | return componentBuilder.create(); 46 | } 47 | 48 | @Override 49 | public ComponentWrapper append(String text) { 50 | ComponentBuilder newBuilder = copyBuilder(componentBuilder); 51 | newBuilder.append(text); 52 | return new BungeeComponentWrapperImpl(newBuilder, true); 53 | } 54 | 55 | @Override 56 | public ComponentWrapper append(ComponentWrapper component) { 57 | ComponentBuilder newBuilder = copyBuilder(componentBuilder); 58 | BungeeComponentWrapperImpl other = (BungeeComponentWrapperImpl) component; 59 | newBuilder.append(other.toBaseComponents()); 60 | return new BungeeComponentWrapperImpl(newBuilder, true); 61 | } 62 | 63 | @Override 64 | public ComponentWrapper color(TextColorWrapper color) { 65 | ComponentBuilder newBuilder = copyBuilder(componentBuilder); 66 | newBuilder.color(switch (color) { 67 | case GOLD -> ChatColor.GOLD; 68 | case RED -> ChatColor.RED; 69 | case DARK_BLUE -> ChatColor.DARK_BLUE; 70 | case GREEN -> ChatColor.GREEN; 71 | }); 72 | return new BungeeComponentWrapperImpl(newBuilder, true); 73 | } 74 | 75 | @Override 76 | public ComponentWrapper decorate(TextDecorationWrapper decoration) { 77 | ComponentBuilder newBuilder = copyBuilder(componentBuilder); 78 | if (decoration == TextDecorationWrapper.BOLD) { 79 | newBuilder.bold(true); 80 | } 81 | return new BungeeComponentWrapperImpl(newBuilder, true); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/KickEventHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.events.PQKickedFromServerEvent; 24 | 25 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 26 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 27 | 28 | import java.util.Locale; 29 | import java.util.Objects; 30 | import java.util.concurrent.locks.Lock; 31 | 32 | /** 33 | * Handles kick events and potentially redirects players to the queue. 34 | */ 35 | public final class KickEventHandler { 36 | private final Config config; 37 | private final QueueEnvironment queueEnvironment; 38 | 39 | public KickEventHandler(Config config, QueueEnvironment queueEnvironment) { 40 | this.config = Objects.requireNonNull(config, "config"); 41 | this.queueEnvironment = Objects.requireNonNull(queueEnvironment, "queueEnvironment"); 42 | } 43 | 44 | /** 45 | * Handles a kick event, potentially redirecting the player to the queue if they were kicked 46 | * from a protected target server due to it being down. 47 | * 48 | * @param event the kick event 49 | */ 50 | public void handleKick(PQKickedFromServerEvent event) { 51 | handleQueueRedirection(event); 52 | handleKickMessage(event); 53 | } 54 | 55 | private void handleQueueRedirection(PQKickedFromServerEvent event) { 56 | QueueGroup group = queueEnvironment.resolveGroupForTarget(event.getKickedFrom()); 57 | boolean kickedFromProtectedTarget = group.getTargetServers().contains(event.getKickedFrom()); 58 | 59 | if (config.ifTargetDownSendToQueue() && kickedFromProtectedTarget) { 60 | String kickReason = event.getKickReason() 61 | .map(s -> s.toLowerCase(Locale.ROOT)) 62 | .orElse("unknown reason"); 63 | 64 | config.downWordList().stream() 65 | .filter(word -> kickReason.contains(word.toLowerCase(Locale.ROOT))) 66 | .findFirst() 67 | .ifPresent(word -> { 68 | event.setCancelServer(group.getQueueServer()); 69 | event.getPlayer().sendMessage(config.ifTargetDownSendToQueueMessage()); 70 | 71 | QueueType queueType = config.getQueueType(event.getPlayer()); 72 | Lock writeLock = queueType.getQueueLock().writeLock(); 73 | writeLock.lock(); 74 | try { 75 | queueType.getQueueMap().put(event.getPlayer().getUniqueId(), 76 | new QueueType.QueuedPlayer(event.getKickedFrom(), QueueType.QueueReason.SERVER_DOWN)); 77 | } finally { 78 | writeLock.unlock(); 79 | } 80 | }); 81 | } 82 | } 83 | 84 | private void handleKickMessage(PQKickedFromServerEvent event) { 85 | if (config.enableKickMessage() && event.willDisconnect()) { 86 | event.setKickMessage(config.kickMessage()); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueuePlacementCoordinator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.events.PQServerPreConnectEvent; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 25 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 26 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 27 | 28 | import java.util.Objects; 29 | import java.util.Optional; 30 | 31 | /** 32 | * Encapsulates the pre-connect logic so that it can be unit tested. 33 | */ 34 | public final class QueuePlacementCoordinator { 35 | private final QueueEnvironment environment; 36 | private final QueueAvailabilityCalculator availabilityCalculator; 37 | private final QueueEntryFactory queueEntryFactory; 38 | 39 | public QueuePlacementCoordinator( 40 | QueueEnvironment environment, 41 | QueueAvailabilityCalculator availabilityCalculator, 42 | QueueEntryFactory queueEntryFactory 43 | ) { 44 | this.environment = Objects.requireNonNull(environment, "environment"); 45 | this.availabilityCalculator = Objects.requireNonNull(availabilityCalculator, "availabilityCalculator"); 46 | this.queueEntryFactory = Objects.requireNonNull(queueEntryFactory, "queueEntryFactory"); 47 | } 48 | 49 | public void handlePreConnect(PQServerPreConnectEvent event) { 50 | PlayerWrapper player = event.getPlayer(); 51 | Config config = environment.config(); 52 | QueueGroup targetGroup = event.getTarget() 53 | .flatMap(name -> config.findGroupByTarget(name)) 54 | .orElse(environment.defaultGroup()); 55 | 56 | if (config.enableSourceServer() && !isSourceToTarget(event, targetGroup)) { 57 | return; 58 | } 59 | 60 | if (!config.enableSourceServer() && player.getCurrentServer().isPresent()) { 61 | return; 62 | } 63 | 64 | if (config.kickWhenDown()) { 65 | for (String server : config.kickWhenDownServers()) { 66 | if (!environment.onlineServers().contains(server)) { 67 | player.disconnect(config.serverDownKickMessage()); 68 | return; 69 | } 70 | } 71 | } 72 | 73 | QueueType type = config.getQueueType(player); 74 | QueueGroup typeGroup = environment.resolveGroupForType(type); 75 | 76 | boolean serverFull = false; 77 | if (config.alwaysQueue() || (serverFull = availabilityCalculator.isServerFull(type))) { 78 | if (player.hasPermission(config.queueBypassPermission())) { 79 | event.setTarget(environment.defaultTarget(typeGroup)); 80 | } else { 81 | queueEntryFactory.enqueue(player, typeGroup, type, event, serverFull, config); 82 | } 83 | } 84 | } 85 | 86 | private boolean isSourceToTarget(PQServerPreConnectEvent event, QueueGroup group) { 87 | Optional previousServer = event.getPlayer().getCurrentServer(); 88 | return previousServer.isPresent() 89 | && group.getSourceServers().contains(previousServer.get()) 90 | && event.getTarget().isPresent() 91 | && group.getTargetServers().contains(event.getTarget().get()); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueEnvironment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 23 | import net.pistonmaster.pistonqueue.shared.config.Config; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 25 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 26 | import net.pistonmaster.pistonqueue.shared.plugin.PistonQueuePlugin; 27 | 28 | import java.util.List; 29 | import java.util.Objects; 30 | import java.util.Set; 31 | import java.util.function.Supplier; 32 | 33 | /** 34 | * Centralizes access to frequently used queue context objects so that the 35 | * extracted logic can stay framework agnostic and easy to test. 36 | */ 37 | public final class QueueEnvironment { 38 | private final PistonQueuePlugin plugin; 39 | private final Supplier configSupplier; 40 | private final Set onlineServers; 41 | 42 | public QueueEnvironment(PistonQueuePlugin plugin, Supplier configSupplier, Set onlineServers) { 43 | this.plugin = Objects.requireNonNull(plugin, "plugin"); 44 | this.configSupplier = Objects.requireNonNull(configSupplier, "configSupplier"); 45 | this.onlineServers = Objects.requireNonNull(onlineServers, "onlineServers"); 46 | } 47 | 48 | @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Plugin API is immutable for consumers") 49 | public PistonQueuePlugin plugin() { 50 | return plugin; 51 | } 52 | 53 | public Config config() { 54 | return configSupplier.get(); 55 | } 56 | 57 | @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Caller needs live view for synchronization") 58 | public Set onlineServers() { 59 | return onlineServers; 60 | } 61 | 62 | public QueueGroup defaultGroup() { 63 | Config config = config(); 64 | QueueGroup group = config.getDefaultGroup(); 65 | if (group != null) { 66 | return group; 67 | } 68 | 69 | QueueType[] queueTypes = config.getAllQueueTypes().toArray(new QueueType[0]); 70 | return new QueueGroup( 71 | "default", 72 | config.queueServer(), 73 | List.of(config.targetServer()), 74 | config.enableSourceServer() ? List.of(config.sourceServer()) : List.of(), 75 | queueTypes 76 | ); 77 | } 78 | 79 | public QueueGroup resolveGroupForTarget(String server) { 80 | if (server == null) { 81 | return defaultGroup(); 82 | } 83 | return config().findGroupByTarget(server).orElse(defaultGroup()); 84 | } 85 | 86 | public QueueGroup resolveGroupForType(QueueType type) { 87 | QueueGroup group = config().getGroupFor(type); 88 | return group != null ? group : defaultGroup(); 89 | } 90 | 91 | public String defaultTarget(QueueGroup group) { 92 | if (group.getTargetServers().isEmpty()) { 93 | return config().targetServer(); 94 | } 95 | return group.getTargetServers().getFirst(); 96 | } 97 | 98 | public boolean isGroupTargetOnline(QueueGroup group) { 99 | return group.getTargetServers().stream().anyMatch(onlineServers::contains); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /bukkit/src/main/java/net/pistonmaster/pistonqueue/bukkit/ProtocolLibWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.bukkit; 21 | 22 | import com.comphenix.protocol.PacketType; 23 | import com.comphenix.protocol.ProtocolLibrary; 24 | import com.comphenix.protocol.ProtocolManager; 25 | import com.comphenix.protocol.events.ListenerPriority; 26 | import com.comphenix.protocol.events.PacketAdapter; 27 | import com.comphenix.protocol.events.PacketContainer; 28 | import com.comphenix.protocol.events.PacketEvent; 29 | import org.bukkit.entity.Player; 30 | 31 | public final class ProtocolLibWrapper { 32 | private ProtocolLibWrapper() { 33 | } 34 | 35 | public static void removeDebug(Player player) { 36 | ProtocolManager manager = ProtocolLibrary.getProtocolManager(); 37 | 38 | PacketContainer packet = manager.createPacket(PacketType.Play.Server.ENTITY_STATUS); 39 | 40 | packet.getIntegers().write(0, player.getEntityId()); 41 | 42 | packet.getBytes().write(0, (byte) 22); 43 | 44 | manager.sendServerPacket(player, packet); 45 | } 46 | 47 | public static void setupProtocolLib(PistonQueueBukkit plugin) { 48 | ProtocolManager manager = ProtocolLibrary.getProtocolManager(); 49 | 50 | if (plugin.isNoChunkPackets()) { 51 | manager.addPacketListener(new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.MAP_CHUNK) { 52 | @Override 53 | public void onPacketSending(PacketEvent event) { 54 | event.setCancelled(true); 55 | } 56 | }); 57 | } 58 | 59 | if (plugin.isNoTimePackets()) { 60 | manager.addPacketListener(new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.UPDATE_TIME) { 61 | @Override 62 | public void onPacketSending(PacketEvent event) { 63 | event.setCancelled(true); 64 | } 65 | }); 66 | } 67 | 68 | if (plugin.isNoHealthPackets()) { 69 | manager.addPacketListener(new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.UPDATE_HEALTH) { 70 | @Override 71 | public void onPacketSending(PacketEvent event) { 72 | event.setCancelled(true); 73 | } 74 | }); 75 | } 76 | 77 | if (plugin.isNoAdvancementPackets()) { 78 | manager.addPacketListener(new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.ADVANCEMENTS) { 79 | @Override 80 | public void onPacketSending(PacketEvent event) { 81 | event.setCancelled(true); 82 | } 83 | }); 84 | } 85 | 86 | if (plugin.isNoExperiencePackets()) { 87 | manager.addPacketListener(new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.EXPERIENCE) { 88 | @Override 89 | public void onPacketSending(PacketEvent event) { 90 | event.setCancelled(true); 91 | } 92 | }); 93 | } 94 | 95 | if (plugin.isShowHeadPacket()) { 96 | manager.addPacketListener(new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.ENTITY_METADATA) { 97 | @Override 98 | public void onPacketSending(PacketEvent event) { 99 | event.setCancelled(true); 100 | } 101 | }); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /placeholder/src/main/java/net/pistonmaster/pistonqueue/placeholder/PistonQueuePlaceholder.java: -------------------------------------------------------------------------------- 1 | package net.pistonmaster.pistonqueue.placeholder; 2 | 3 | import com.google.common.io.ByteArrayDataInput; 4 | import com.google.common.io.ByteStreams; 5 | import lombok.Getter; 6 | import net.pistonmaster.pistonutils.update.GitHubUpdateChecker; 7 | import net.pistonmaster.pistonutils.update.SemanticVersion; 8 | import org.bukkit.ChatColor; 9 | import org.bukkit.entity.Player; 10 | import org.bukkit.plugin.java.JavaPlugin; 11 | import org.bukkit.plugin.messaging.PluginMessageListener; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.io.IOException; 15 | import java.util.Map; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | import java.util.logging.Level; 18 | import java.util.logging.Logger; 19 | 20 | @Getter 21 | public final class PistonQueuePlaceholder extends JavaPlugin implements PluginMessageListener { 22 | private final Map onlineQueue = new ConcurrentHashMap<>(); 23 | private final Map onlineTarget = new ConcurrentHashMap<>(); 24 | 25 | @Override 26 | public void onEnable() { 27 | Logger log = getLogger(); 28 | 29 | checkIfBungee(); 30 | 31 | log.info(ChatColor.BLUE + "Registering messaging channel"); 32 | getServer().getMessenger().registerIncomingPluginChannel(this, "piston:queue", this); 33 | 34 | log.info(ChatColor.BLUE + "Registering PAPI expansion"); 35 | new PAPIExpansion(this).register(); 36 | 37 | log.info(ChatColor.BLUE + "Checking for a newer version"); 38 | try { 39 | String currentVersionString = this.getDescription().getVersion(); 40 | SemanticVersion gitHubVersion = new GitHubUpdateChecker() 41 | .getVersion("https://api.github.com/repos/AlexProgrammerDE/PistonQueue/releases/latest"); 42 | SemanticVersion currentVersion = SemanticVersion.fromString(currentVersionString); 43 | 44 | if (gitHubVersion.isNewerThan(currentVersion)) { 45 | log.info(ChatColor.RED + "There is an update available!"); 46 | log.info(ChatColor.RED + "Current version: " + currentVersionString + " New version: " + gitHubVersion); 47 | log.info(ChatColor.RED + "Download it at: https://modrinth.com/plugin/pistonqueue"); 48 | } else { 49 | log.info(ChatColor.BLUE + "You're up to date!"); 50 | } 51 | } catch (IOException e) { 52 | log.log(Level.SEVERE, "Could not check for updates!", e); 53 | } 54 | 55 | log.info(ChatColor.BLUE + "Successfully enabled!"); 56 | } 57 | 58 | @Override 59 | public void onPluginMessageReceived(String channel, @NotNull Player player, byte[] bytes) { 60 | if (!"piston:queue".equalsIgnoreCase(channel)) { 61 | return; 62 | } 63 | 64 | @SuppressWarnings({"UnstableApiUsage"}) 65 | ByteArrayDataInput in = ByteStreams.newDataInput(bytes); 66 | String subChannel = in.readUTF(); 67 | 68 | if ("onlineQueue".equalsIgnoreCase(subChannel)) { 69 | int count = in.readInt(); 70 | 71 | for (int i = 0; i < count; i++) { 72 | String queue = in.readUTF(); 73 | int online = in.readInt(); 74 | 75 | onlineQueue.put(queue, online); 76 | } 77 | } else if ("onlineTarget".equalsIgnoreCase(subChannel)) { 78 | int count = in.readInt(); 79 | 80 | for (int i = 0; i < count; i++) { 81 | String queue = in.readUTF(); 82 | int online = in.readInt(); 83 | 84 | onlineTarget.put(queue, online); 85 | } 86 | } 87 | } 88 | 89 | private void checkIfBungee() { 90 | if (!isSpigot()) { 91 | getLogger().severe(ChatColor.RED + "You probably run CraftBukkit. Update at least to Spigot for this plugin to work!"); 92 | getLogger().severe(ChatColor.RED + "Plugin disabled!"); 93 | getServer().getPluginManager().disablePlugin(this); 94 | } 95 | } 96 | 97 | private boolean isSpigot() { 98 | try { 99 | Class.forName("org.spigotmc.SpigotConfig"); 100 | return true; 101 | } catch (ClassNotFoundException e) { 102 | return false; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /shared/src/test/java/net/pistonmaster/pistonqueue/shared/queue/logic/UsernameValidatorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import org.junit.jupiter.api.Test; 24 | 25 | import static org.junit.jupiter.api.Assertions.*; 26 | 27 | class UsernameValidatorTest { 28 | 29 | @Test 30 | void allowsValidUsernameWhenRegexEnabled() { 31 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 32 | config.setEnableUsernameRegex(true); 33 | config.setUsernameRegex("[A-Za-z0-9_]{3,16}"); 34 | config.setUsernameRegexMessage("Invalid username format"); 35 | 36 | UsernameValidator validator = new UsernameValidator(config); 37 | QueueTestUtils.TestPreLoginEvent event = QueueTestUtils.preLoginEvent("ValidUser123"); 38 | 39 | validator.validateUsername(event); 40 | 41 | assertFalse(event.isCancelled()); 42 | assertTrue(event.getCancelReason().isEmpty()); 43 | } 44 | 45 | @Test 46 | void rejectsInvalidUsernameWhenRegexEnabled() { 47 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 48 | config.setEnableUsernameRegex(true); 49 | config.setUsernameRegex("[A-Za-z0-9_]{3,16}"); 50 | config.setUsernameRegexMessage("Username must match %regex%"); 51 | 52 | UsernameValidator validator = new UsernameValidator(config); 53 | QueueTestUtils.TestPreLoginEvent event = QueueTestUtils.preLoginEvent("Invalid@User!"); 54 | 55 | validator.validateUsername(event); 56 | 57 | assertTrue(event.isCancelled()); 58 | assertEquals("Username must match [A-Za-z0-9_]{3,16}", event.getCancelReason().orElseThrow()); 59 | } 60 | 61 | @Test 62 | void allowsAnyUsernameWhenRegexDisabled() { 63 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 64 | config.setEnableUsernameRegex(false); 65 | config.setUsernameRegex("[A-Za-z0-9_]{3,16}"); 66 | 67 | UsernameValidator validator = new UsernameValidator(config); 68 | QueueTestUtils.TestPreLoginEvent event = QueueTestUtils.preLoginEvent("Any@User!123"); 69 | 70 | validator.validateUsername(event); 71 | 72 | assertFalse(event.isCancelled()); 73 | } 74 | 75 | @Test 76 | void doesNotProcessAlreadyCancelledEvent() { 77 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 78 | config.setEnableUsernameRegex(true); 79 | config.setUsernameRegex("[A-Za-z0-9_]{3,16}"); 80 | 81 | UsernameValidator validator = new UsernameValidator(config); 82 | QueueTestUtils.TestPreLoginEvent event = QueueTestUtils.preLoginEvent("Invalid@User!"); 83 | event.setCancelled("Already cancelled"); 84 | 85 | validator.validateUsername(event); 86 | 87 | assertTrue(event.isCancelled()); 88 | assertEquals("Already cancelled", event.getCancelReason().orElseThrow()); 89 | } 90 | 91 | @Test 92 | void replacesRegexPlaceholderInMessage() { 93 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 94 | config.setEnableUsernameRegex(true); 95 | config.setUsernameRegex("[A-Za-z]{5,}"); 96 | config.setUsernameRegexMessage("Username must match pattern: %regex%"); 97 | 98 | UsernameValidator validator = new UsernameValidator(config); 99 | QueueTestUtils.TestPreLoginEvent event = QueueTestUtils.preLoginEvent("123"); 100 | 101 | validator.validateUsername(event); 102 | 103 | assertTrue(event.isCancelled()); 104 | assertEquals("Username must match pattern: [A-Za-z]{5,}", event.getCancelReason().orElseThrow()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /shared/src/test/java/net/pistonmaster/pistonqueue/shared/queue/logic/ShadowBanKickHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.BanType; 24 | import net.pistonmaster.pistonqueue.shared.utils.StorageTool; 25 | import org.junit.jupiter.api.Test; 26 | 27 | import java.time.Instant; 28 | import java.util.Date; 29 | 30 | import static org.junit.jupiter.api.Assertions.*; 31 | 32 | class ShadowBanKickHandlerTest { 33 | 34 | @Test 35 | void kicksShadowBannedPlayerWhenKickTypeEnabled() { 36 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 37 | config.setShadowBanType(BanType.KICK); 38 | config.setServerDownKickMessage("You are shadow banned"); 39 | 40 | ShadowBanKickHandler handler = new ShadowBanKickHandler(config); 41 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 42 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("BannedPlayer"); 43 | 44 | // Simulate shadow ban 45 | QueueTestUtils.ensureStorageToolInitialized(); 46 | StorageTool.shadowBanPlayer("BannedPlayer", Date.from(Instant.ofEpochMilli(System.currentTimeMillis() + 86400000))); // 1 day from now 47 | 48 | handler.handleShadowBanKick(player); 49 | 50 | assertTrue(player.isDisconnected()); 51 | assertEquals("You are shadow banned", player.getDisconnectMessage()); 52 | } 53 | 54 | @Test 55 | void doesNotKickNonShadowBannedPlayer() { 56 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 57 | config.setShadowBanType(BanType.KICK); 58 | 59 | ShadowBanKickHandler handler = new ShadowBanKickHandler(config); 60 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 61 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("GoodPlayer"); 62 | 63 | handler.handleShadowBanKick(player); 64 | 65 | assertFalse(player.isDisconnected()); 66 | } 67 | 68 | @Test 69 | void doesNotKickShadowBannedPlayerWhenKickTypeDisabled() { 70 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 71 | config.setShadowBanType(BanType.LOOP); // Not KICK type 72 | 73 | ShadowBanKickHandler handler = new ShadowBanKickHandler(config); 74 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 75 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("BannedPlayer"); 76 | 77 | // Simulate shadow ban 78 | QueueTestUtils.ensureStorageToolInitialized(); 79 | StorageTool.shadowBanPlayer("BannedPlayer", Date.from(Instant.ofEpochMilli(System.currentTimeMillis() + 86400000))); // 1 day from now 80 | 81 | handler.handleShadowBanKick(player); 82 | 83 | assertFalse(player.isDisconnected()); 84 | } 85 | 86 | @Test 87 | void usesCorrectKickMessage() { 88 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 89 | config.setShadowBanType(BanType.KICK); 90 | config.setServerDownKickMessage("Custom shadow ban message"); 91 | 92 | ShadowBanKickHandler handler = new ShadowBanKickHandler(config); 93 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 94 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("BannedPlayer"); 95 | 96 | // Simulate shadow ban 97 | QueueTestUtils.ensureStorageToolInitialized(); 98 | StorageTool.shadowBanPlayer("BannedPlayer", Date.from(Instant.ofEpochMilli(System.currentTimeMillis() + 86400000))); // 1 day from now 99 | 100 | handler.handleShadowBanKick(player); 101 | 102 | assertTrue(player.isDisconnected()); 103 | assertEquals("Custom shadow ban message", player.getDisconnectMessage()); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /shared/src/test/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueAvailabilityCalculatorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueType.QueueReason; 25 | import org.junit.jupiter.api.Test; 26 | 27 | import java.util.UUID; 28 | 29 | import static org.junit.jupiter.api.Assertions.*; 30 | 31 | class QueueAvailabilityCalculatorTest { 32 | 33 | @Test 34 | void getFreeSlotsReturnsCorrectCount() { 35 | Config config = QueueTestUtils.createConfigWithSingleQueueType(10); 36 | QueueType type = QueueTestUtils.defaultQueueType(config); 37 | type.setReservedSlots(10); 38 | type.getPlayersWithTypeInTarget().set(3); 39 | 40 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 41 | 42 | int freeSlots = calculator.getFreeSlots(type); 43 | 44 | assertEquals(7, freeSlots); 45 | } 46 | 47 | @Test 48 | void getFreeSlotsReturnsZeroWhenFull() { 49 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 50 | QueueType type = QueueTestUtils.defaultQueueType(config); 51 | type.setReservedSlots(5); 52 | type.getPlayersWithTypeInTarget().set(5); 53 | 54 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 55 | 56 | int freeSlots = calculator.getFreeSlots(type); 57 | 58 | assertEquals(0, freeSlots); 59 | } 60 | 61 | @Test 62 | void isTargetFullReturnsTrueWhenNoFreeSlots() { 63 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 64 | QueueType type = QueueTestUtils.defaultQueueType(config); 65 | type.setReservedSlots(5); 66 | type.getPlayersWithTypeInTarget().set(5); 67 | 68 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 69 | 70 | boolean isFull = calculator.isTargetFull(type); 71 | 72 | assertTrue(isFull); 73 | } 74 | 75 | @Test 76 | void isTargetFullReturnsFalseWhenHasFreeSlots() { 77 | Config config = QueueTestUtils.createConfigWithSingleQueueType(10); 78 | QueueType type = QueueTestUtils.defaultQueueType(config); 79 | type.setReservedSlots(10); 80 | type.getPlayersWithTypeInTarget().set(7); 81 | 82 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 83 | 84 | boolean isFull = calculator.isTargetFull(type); 85 | 86 | assertFalse(isFull); 87 | } 88 | 89 | @Test 90 | void isServerFullReturnsTrueWhenTargetFull() { 91 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 92 | QueueType type = QueueTestUtils.defaultQueueType(config); 93 | type.setReservedSlots(5); 94 | type.getPlayersWithTypeInTarget().set(5); 95 | 96 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 97 | 98 | boolean isFull = calculator.isServerFull(type); 99 | 100 | assertTrue(isFull); 101 | } 102 | 103 | @Test 104 | void isServerFullReturnsTrueWhenHasQueuedPlayers() { 105 | Config config = QueueTestUtils.createConfigWithSingleQueueType(10); 106 | QueueType type = QueueTestUtils.defaultQueueType(config); 107 | type.setReservedSlots(10); 108 | type.getPlayersWithTypeInTarget().set(0); 109 | type.getQueueMap().put(UUID.randomUUID(), new QueueType.QueuedPlayer("target", QueueReason.SERVER_FULL)); 110 | 111 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 112 | 113 | boolean isFull = calculator.isServerFull(type); 114 | 115 | assertTrue(isFull); 116 | } 117 | 118 | @Test 119 | void isServerFullReturnsFalseWhenNotFullAndNoQueue() { 120 | Config config = QueueTestUtils.createConfigWithSingleQueueType(10); 121 | QueueType type = QueueTestUtils.defaultQueueType(config); 122 | type.setReservedSlots(10); 123 | type.getPlayersWithTypeInTarget().set(5); 124 | 125 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 126 | 127 | boolean isFull = calculator.isServerFull(type); 128 | 129 | assertFalse(isFull); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /bungee/src/main/java/net/pistonmaster/pistonqueue/bungee/listeners/QueueListenerBungee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.bungee.listeners; 21 | 22 | import net.md_5.bungee.api.chat.TextComponent; 23 | import net.md_5.bungee.api.event.PostLoginEvent; 24 | import net.md_5.bungee.api.event.PreLoginEvent; 25 | import net.md_5.bungee.api.event.ServerConnectEvent; 26 | import net.md_5.bungee.api.event.ServerKickEvent; 27 | import net.md_5.bungee.api.plugin.Listener; 28 | import net.md_5.bungee.event.EventHandler; 29 | import net.pistonmaster.pistonqueue.bungee.PistonQueueBungee; 30 | import net.pistonmaster.pistonqueue.bungee.utils.ChatUtils; 31 | import net.pistonmaster.pistonqueue.shared.events.PQKickedFromServerEvent; 32 | import net.pistonmaster.pistonqueue.shared.events.PQPreLoginEvent; 33 | import net.pistonmaster.pistonqueue.shared.events.PQServerPreConnectEvent; 34 | import net.pistonmaster.pistonqueue.shared.queue.QueueListenerShared; 35 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 36 | 37 | import java.util.Optional; 38 | 39 | public final class QueueListenerBungee extends QueueListenerShared implements Listener { 40 | private final PistonQueueBungee plugin; 41 | 42 | public QueueListenerBungee(PistonQueueBungee plugin) { 43 | super(plugin); 44 | this.plugin = plugin; 45 | } 46 | 47 | @EventHandler 48 | public void onPreLogin(PreLoginEvent event) { 49 | onPreLogin(wrap(event)); 50 | } 51 | 52 | @EventHandler 53 | public void onPostLogin(PostLoginEvent event) { 54 | onPostLogin(plugin.wrapPlayer(event.getPlayer())); 55 | } 56 | 57 | @EventHandler 58 | public void onSend(ServerConnectEvent event) { 59 | onPreConnect(wrap(event)); 60 | } 61 | 62 | @EventHandler 63 | public void onKick(ServerKickEvent event) { 64 | onKick(wrap(event)); 65 | } 66 | 67 | private PQServerPreConnectEvent wrap(ServerConnectEvent event) { 68 | return new PQServerPreConnectEvent() { 69 | @Override 70 | public PlayerWrapper getPlayer() { 71 | return plugin.wrapPlayer(event.getPlayer()); 72 | } 73 | 74 | @Override 75 | public Optional getTarget() { 76 | return Optional.of(event.getTarget().getName()); 77 | } 78 | 79 | @Override 80 | public void setTarget(String server) { 81 | event.setTarget(plugin.getProxy().getServerInfo(server)); 82 | } 83 | }; 84 | } 85 | 86 | private PQKickedFromServerEvent wrap(ServerKickEvent event) { 87 | return new PQKickedFromServerEvent() { 88 | @Override 89 | public void setCancelServer(String server) { 90 | event.setCancelServer(plugin.getProxy().getServerInfo(server)); 91 | event.setCancelled(true); 92 | } 93 | 94 | @Override 95 | public void setKickMessage(String message) { 96 | event.setReason(ChatUtils.parseToComponent(plugin.getConfiguration(), message)); 97 | } 98 | 99 | @Override 100 | public PlayerWrapper getPlayer() { 101 | return plugin.wrapPlayer(event.getPlayer()); 102 | } 103 | 104 | @Override 105 | public String getKickedFrom() { 106 | return event.getKickedFrom().getName(); 107 | } 108 | 109 | @Override 110 | public Optional getKickReason() { 111 | return Optional.ofNullable(event.getReason()).map(TextComponent::toLegacyText); 112 | } 113 | 114 | @Override 115 | public boolean willDisconnect() { 116 | return !event.isCancelled(); 117 | } 118 | }; 119 | } 120 | 121 | private PQPreLoginEvent wrap(PreLoginEvent event) { 122 | return new PQPreLoginEvent() { 123 | @Override 124 | public boolean isCancelled() { 125 | return event.isCancelled(); 126 | } 127 | 128 | @Override 129 | public void setCancelled(String reason) { 130 | event.setReason(ChatUtils.parseToComponent(plugin.getConfiguration(), reason)); 131 | event.setCancelled(true); 132 | } 133 | 134 | @Override 135 | public String getUsername() { 136 | return event.getConnection().getName(); 137 | } 138 | }; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/QueueListenerShared.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue; 21 | 22 | import lombok.Getter; 23 | import net.pistonmaster.pistonqueue.shared.config.Config; 24 | import net.pistonmaster.pistonqueue.shared.events.PQKickedFromServerEvent; 25 | import net.pistonmaster.pistonqueue.shared.events.PQPreLoginEvent; 26 | import net.pistonmaster.pistonqueue.shared.events.PQServerPreConnectEvent; 27 | import net.pistonmaster.pistonqueue.shared.plugin.PistonQueuePlugin; 28 | import net.pistonmaster.pistonqueue.shared.queue.logic.KickEventHandler; 29 | import net.pistonmaster.pistonqueue.shared.queue.logic.QueueAvailabilityCalculator; 30 | import net.pistonmaster.pistonqueue.shared.queue.logic.QueueCleaner; 31 | import net.pistonmaster.pistonqueue.shared.queue.logic.QueueConnector; 32 | import net.pistonmaster.pistonqueue.shared.queue.logic.QueueEntryFactory; 33 | import net.pistonmaster.pistonqueue.shared.queue.logic.QueueEnvironment; 34 | import net.pistonmaster.pistonqueue.shared.queue.logic.QueueMoveProcessor; 35 | import net.pistonmaster.pistonqueue.shared.queue.logic.QueuePlacementCoordinator; 36 | import net.pistonmaster.pistonqueue.shared.queue.logic.QueueRecoveryHandler; 37 | import net.pistonmaster.pistonqueue.shared.queue.logic.ShadowBanKickHandler; 38 | import net.pistonmaster.pistonqueue.shared.queue.logic.ShadowBanService; 39 | import net.pistonmaster.pistonqueue.shared.queue.logic.StorageShadowBanService; 40 | import net.pistonmaster.pistonqueue.shared.queue.logic.UsernameValidator; 41 | import net.pistonmaster.pistonqueue.shared.utils.StorageTool; 42 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 43 | 44 | import java.util.Locale; 45 | import java.util.Set; 46 | import java.util.concurrent.ConcurrentHashMap; 47 | import java.util.concurrent.locks.Lock; 48 | 49 | public abstract class QueueListenerShared { 50 | private final PistonQueuePlugin plugin; 51 | @Getter 52 | private final Set onlineServers = ConcurrentHashMap.newKeySet(); 53 | private final QueueEnvironment queueEnvironment; 54 | private final QueuePlacementCoordinator queuePlacementCoordinator; 55 | private final QueueMoveProcessor queueMoveProcessor; 56 | private final UsernameValidator usernameValidator; 57 | private final ShadowBanKickHandler shadowBanKickHandler; 58 | private final KickEventHandler kickEventHandler; 59 | 60 | protected QueueListenerShared(PistonQueuePlugin plugin) { 61 | this.plugin = plugin; 62 | this.queueEnvironment = new QueueEnvironment(plugin, this::currentConfig, onlineServers); 63 | Config config = currentConfig(); 64 | this.usernameValidator = new UsernameValidator(config); 65 | this.shadowBanKickHandler = new ShadowBanKickHandler(config); 66 | this.kickEventHandler = new KickEventHandler(config, queueEnvironment); 67 | 68 | QueueAvailabilityCalculator availabilityCalculator = new QueueAvailabilityCalculator(); 69 | QueueEntryFactory queueEntryFactory = new QueueEntryFactory(queueEnvironment); 70 | this.queuePlacementCoordinator = new QueuePlacementCoordinator(queueEnvironment, availabilityCalculator, queueEntryFactory); 71 | QueueCleaner queueCleaner = new QueueCleaner(queueEnvironment); 72 | QueueRecoveryHandler recoveryHandler = new QueueRecoveryHandler(queueEnvironment); 73 | ShadowBanService shadowBanService = new StorageShadowBanService(); 74 | QueueConnector queueConnector = new QueueConnector(queueEnvironment, availabilityCalculator, shadowBanService); 75 | this.queueMoveProcessor = new QueueMoveProcessor(queueEnvironment, queueCleaner, recoveryHandler, queueConnector); 76 | } 77 | 78 | protected void onPreLogin(PQPreLoginEvent event) { 79 | usernameValidator.validateUsername(event); 80 | } 81 | 82 | protected void onPostLogin(PlayerWrapper player) { 83 | shadowBanKickHandler.handleShadowBanKick(player); 84 | } 85 | 86 | protected void onKick(PQKickedFromServerEvent event) { 87 | kickEventHandler.handleKick(event); 88 | } 89 | 90 | protected void onPreConnect(PQServerPreConnectEvent event) { 91 | queuePlacementCoordinator.handlePreConnect(event); 92 | } 93 | 94 | public void moveQueue() { 95 | queueMoveProcessor.processQueues(); 96 | } 97 | 98 | private Config currentConfig() { 99 | return plugin.getConfiguration(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /shared/src/test/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueCleanerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 25 | import net.pistonmaster.pistonqueue.shared.queue.QueueType.QueueReason; 26 | import org.junit.jupiter.api.Test; 27 | 28 | import java.util.Set; 29 | 30 | import static org.junit.jupiter.api.Assertions.*; 31 | 32 | class QueueCleanerTest { 33 | 34 | @Test 35 | void removesStaleEntriesWhenPlayerDisconnected() { 36 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 37 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 38 | QueueGroup group = QueueTestUtils.defaultGroup(config); 39 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 40 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 41 | QueueCleaner cleaner = new QueueCleaner(environment); 42 | 43 | QueueType type = QueueTestUtils.defaultQueueType(config); 44 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("Stale"); 45 | type.getQueueMap().put(player.getUniqueId(), new QueueType.QueuedPlayer("target", QueueReason.SERVER_FULL)); 46 | // Player is not on the queue server anymore 47 | 48 | cleaner.cleanGroup(group); 49 | 50 | assertTrue(type.getQueueMap().isEmpty()); 51 | } 52 | 53 | @Test 54 | void keepsActiveQueueEntries() { 55 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 56 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 57 | QueueGroup group = QueueTestUtils.defaultGroup(config); 58 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 59 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 60 | QueueCleaner cleaner = new QueueCleaner(environment); 61 | 62 | QueueType type = QueueTestUtils.defaultQueueType(config); 63 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("Active"); 64 | player.setCurrentServer(group.getQueueServer()); 65 | type.getQueueMap().put(player.getUniqueId(), new QueueType.QueuedPlayer("target", QueueReason.SERVER_FULL)); 66 | 67 | cleaner.cleanGroup(group); 68 | 69 | assertEquals(1, type.getQueueMap().size()); 70 | assertTrue(type.getQueueMap().containsKey(player.getUniqueId())); 71 | } 72 | 73 | @Test 74 | void handlesEmptyQueue() { 75 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 76 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 77 | QueueGroup group = QueueTestUtils.defaultGroup(config); 78 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 79 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 80 | QueueCleaner cleaner = new QueueCleaner(environment); 81 | 82 | cleaner.cleanGroup(group); 83 | 84 | // Should not throw any exceptions 85 | } 86 | 87 | @Test 88 | void removesMultipleStaleEntries() { 89 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 90 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 91 | QueueGroup group = QueueTestUtils.defaultGroup(config); 92 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 93 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 94 | QueueCleaner cleaner = new QueueCleaner(environment); 95 | 96 | QueueType type = QueueTestUtils.defaultQueueType(config); 97 | QueueTestUtils.TestPlayer player1 = plugin.registerPlayer("Stale1"); 98 | QueueTestUtils.TestPlayer player2 = plugin.registerPlayer("Stale2"); 99 | type.getQueueMap().put(player1.getUniqueId(), new QueueType.QueuedPlayer("target", QueueReason.SERVER_FULL)); 100 | type.getQueueMap().put(player2.getUniqueId(), new QueueType.QueuedPlayer("target", QueueReason.SERVER_FULL)); 101 | // Both players are not on the queue server 102 | 103 | cleaner.cleanGroup(group); 104 | 105 | assertTrue(type.getQueueMap().isEmpty()); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /bukkit/src/main/java/net/pistonmaster/pistonqueue/bukkit/config/BukkitConfig.java: -------------------------------------------------------------------------------- 1 | package net.pistonmaster.pistonqueue.bukkit.config; 2 | 3 | import de.exlll.configlib.Comment; 4 | import de.exlll.configlib.Configuration; 5 | 6 | @Configuration 7 | public final class BukkitConfig { 8 | @Comment("Force the user to remain in a certain position.") 9 | public LocationSection location = new LocationSection(); 10 | 11 | @Comment( 12 | "Visibility controls. Hiding players also disables join/leave messages." 13 | ) 14 | public VisibilitySection visibility = new VisibilitySection(); 15 | 16 | @Comment("Options that prevent communication while waiting in queue.") 17 | public CommunicationSection communication = new CommunicationSection(); 18 | 19 | @Comment("Audio feedback played when the proxy sends plugin messages.") 20 | public AudioSection audio = new AudioSection(); 21 | 22 | @Comment("General player protection toggles applied while in queue.") 23 | public ProtectionsSection protections = new ProtectionsSection(); 24 | 25 | @Comment({ "ProtocolLib specific options", "Only applied when ProtocolLib is present" }) 26 | public ProtocolLibSection protocolLib = new ProtocolLibSection(); 27 | 28 | @Configuration 29 | public static final class LocationSection { 30 | @Comment("Force the user to remain in a certain position.") 31 | public boolean enabled = true; 32 | 33 | @Comment("Forced world") 34 | public String world = "world_the_end"; 35 | 36 | @Comment("Forced coordinates") 37 | public CoordinatesSection coordinates = new CoordinatesSection(); 38 | } 39 | 40 | @Configuration 41 | public static final class CoordinatesSection { 42 | @Comment("Forced x coordinate") 43 | public int x = 500; 44 | 45 | @Comment("Forced y coordinate") 46 | public int y = 256; 47 | 48 | @Comment("Forced z coordinate") 49 | public int z = 500; 50 | } 51 | 52 | @Configuration 53 | public static final class VisibilitySection { 54 | @Comment( 55 | "Hide players from each other so that it looks like every user is alone in the world." 56 | ) 57 | public boolean hidePlayers = true; 58 | 59 | @Comment("Prevent players from moving.") 60 | public boolean restrictMovement = true; 61 | 62 | @Comment("Force players to remain in a gamemode.") 63 | public ForceGamemodeSection forceGamemode = new ForceGamemodeSection(); 64 | 65 | @Comment("Show a player's own name to themselves in spectator menu.") 66 | public TeamSection team = new TeamSection(); 67 | } 68 | 69 | @Configuration 70 | public static final class ForceGamemodeSection { 71 | @Comment("Force players to remain in a gamemode.") 72 | public boolean enabled = true; 73 | 74 | @Comment("The gamemode to force players to remain in.") 75 | public String mode = "spectator"; 76 | } 77 | 78 | @Configuration 79 | public static final class TeamSection { 80 | @Comment("Show a player's own name to themselves in spectator menu.") 81 | public boolean enabled = false; 82 | 83 | @Comment("The team name the user sees. (Valid placeholders: %player_name%, %random%)") 84 | public String name = "%player_name%"; 85 | } 86 | 87 | @Configuration 88 | public static final class CommunicationSection { 89 | @Comment("Don't allow players to chat.") 90 | public boolean disableChat = true; 91 | 92 | @Comment("Don't allow commands.") 93 | public boolean disableCommands = true; 94 | } 95 | 96 | @Configuration 97 | public static final class AudioSection { 98 | @Comment("Plays an XP sound when the proxy sends a plugin message.") 99 | public boolean playXpSound = true; 100 | } 101 | 102 | @Configuration 103 | public static final class ProtectionsSection { 104 | @Comment("Prevents players from gaining experience.") 105 | public boolean preventExperience = true; 106 | 107 | @Comment("Prevents players from getting damage.") 108 | public boolean preventDamage = true; 109 | 110 | @Comment("Prevents players from gaining hunger.") 111 | public boolean preventHunger = true; 112 | } 113 | 114 | @Configuration 115 | public static final class ProtocolLibSection { 116 | @Comment("Doesn't show the client's position on F3.") 117 | public boolean disableDebug = true; 118 | 119 | @Comment("Packets that should be suppressed when ProtocolLib is installed.") 120 | public SuppressPacketsSection suppressPackets = new SuppressPacketsSection(); 121 | 122 | @Comment( 123 | "Does not send entity metadata anymore, causing that the entire player head is shown while in spectator." 124 | ) 125 | public boolean showFullHead = true; 126 | } 127 | 128 | @Configuration 129 | public static final class SuppressPacketsSection { 130 | @Comment("Do not send chunk data packets.") 131 | public boolean chunk = true; 132 | 133 | @Comment("Do not send time packets.") 134 | public boolean time = true; 135 | 136 | @Comment("Do not send health packets.") 137 | public boolean health = true; 138 | 139 | @Comment("Do not send advancement packets.") 140 | public boolean advancement = true; 141 | 142 | @Comment("Do not send experience packets.") 143 | public boolean experience = true; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /velocity/src/main/java/net/pistonmaster/pistonqueue/velocity/listeners/QueueListenerVelocity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.velocity.listeners; 21 | 22 | import com.velocitypowered.api.event.Subscribe; 23 | import com.velocitypowered.api.event.connection.PostLoginEvent; 24 | import com.velocitypowered.api.event.connection.PreLoginEvent; 25 | import com.velocitypowered.api.event.player.KickedFromServerEvent; 26 | import com.velocitypowered.api.event.player.ServerPreConnectEvent; 27 | import com.velocitypowered.api.proxy.server.RegisteredServer; 28 | import com.velocitypowered.api.proxy.server.ServerInfo; 29 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 30 | import net.pistonmaster.pistonqueue.shared.events.PQKickedFromServerEvent; 31 | import net.pistonmaster.pistonqueue.shared.events.PQPreLoginEvent; 32 | import net.pistonmaster.pistonqueue.shared.events.PQServerPreConnectEvent; 33 | import net.pistonmaster.pistonqueue.shared.queue.QueueListenerShared; 34 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 35 | import net.pistonmaster.pistonqueue.velocity.PistonQueueVelocity; 36 | import net.pistonmaster.pistonqueue.velocity.utils.ChatUtils; 37 | 38 | import java.util.Optional; 39 | 40 | public final class QueueListenerVelocity extends QueueListenerShared { 41 | private final PistonQueueVelocity plugin; 42 | 43 | public QueueListenerVelocity(PistonQueueVelocity plugin) { 44 | super(plugin); 45 | this.plugin = plugin; 46 | } 47 | 48 | @Subscribe 49 | public void onPreLogin(PreLoginEvent event) { 50 | onPreLogin(wrap(event)); 51 | } 52 | 53 | @Subscribe 54 | public void onPostLogin(PostLoginEvent event) { 55 | onPostLogin(plugin.wrapPlayer(event.getPlayer())); 56 | } 57 | 58 | @Subscribe 59 | public void onKick(KickedFromServerEvent event) { 60 | onKick(wrap(event)); 61 | } 62 | 63 | @Subscribe 64 | public void onSend(ServerPreConnectEvent event) { 65 | onPreConnect(wrap(event)); 66 | } 67 | 68 | private PQServerPreConnectEvent wrap(ServerPreConnectEvent event) { 69 | return new PQServerPreConnectEvent() { 70 | @Override 71 | public PlayerWrapper getPlayer() { 72 | return plugin.wrapPlayer(event.getPlayer()); 73 | } 74 | 75 | @Override 76 | public Optional getTarget() { 77 | return event.getResult().getServer().map(RegisteredServer::getServerInfo).map(ServerInfo::getName); 78 | } 79 | 80 | @Override 81 | public void setTarget(String server) { 82 | event.setResult(ServerPreConnectEvent.ServerResult.allowed(plugin.getProxyServer().getServer(server).orElseThrow(() -> 83 | new IllegalArgumentException("Server %s not found".formatted(server))))); 84 | } 85 | }; 86 | } 87 | 88 | private PQKickedFromServerEvent wrap(KickedFromServerEvent event) { 89 | return new PQKickedFromServerEvent() { 90 | @Override 91 | public void setCancelServer(String server) { 92 | event.setResult(KickedFromServerEvent.RedirectPlayer.create(plugin.getProxyServer().getServer(server).orElseThrow(() -> 93 | new IllegalArgumentException("Server %s not found".formatted(server))))); 94 | } 95 | 96 | @Override 97 | public void setKickMessage(String message) { 98 | event.setResult(KickedFromServerEvent.DisconnectPlayer.create(ChatUtils.parseToComponent(plugin.getConfiguration(), message))); 99 | } 100 | 101 | @Override 102 | public PlayerWrapper getPlayer() { 103 | return plugin.wrapPlayer(event.getPlayer()); 104 | } 105 | 106 | @Override 107 | public String getKickedFrom() { 108 | return event.getServer().getServerInfo().getName(); 109 | } 110 | 111 | @Override 112 | public Optional getKickReason() { 113 | return event.getServerKickReason().map(LegacyComponentSerializer.legacySection()::serialize); 114 | } 115 | 116 | @Override 117 | public boolean willDisconnect() { 118 | return event.getResult().isAllowed(); 119 | } 120 | }; 121 | } 122 | 123 | private PQPreLoginEvent wrap(PreLoginEvent event) { 124 | return new PQPreLoginEvent() { 125 | @Override 126 | public boolean isCancelled() { 127 | return event.getResult() != PreLoginEvent.PreLoginComponentResult.allowed(); 128 | } 129 | 130 | @Override 131 | public void setCancelled(String reason) { 132 | event.setResult(PreLoginEvent.PreLoginComponentResult.denied(ChatUtils.parseToComponent(plugin.getConfiguration(), reason))); 133 | } 134 | 135 | @Override 136 | public String getUsername() { 137 | return event.getUsername(); 138 | } 139 | }; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /shared/src/test/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueRecoveryHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 25 | import net.pistonmaster.pistonqueue.shared.queue.QueueType.QueueReason; 26 | import org.junit.jupiter.api.Test; 27 | 28 | import java.util.Set; 29 | 30 | import static org.junit.jupiter.api.Assertions.*; 31 | 32 | class QueueRecoveryHandlerTest { 33 | 34 | @Test 35 | void recoversPlayerOnQueueServer() { 36 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 37 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 38 | QueueGroup group = QueueTestUtils.defaultGroup(config); 39 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer(), group.getTargetServers().getFirst()); 40 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 41 | QueueRecoveryHandler recoveryHandler = new QueueRecoveryHandler(environment); 42 | 43 | QueueType type = QueueTestUtils.defaultQueueType(config); 44 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("Recover"); 45 | player.setCurrentServer(group.getQueueServer()); 46 | 47 | recoveryHandler.recoverPlayer(player); 48 | 49 | assertTrue(type.getQueueMap().containsKey(player.getUniqueId())); 50 | assertTrue(player.getMessages().stream().anyMatch(msg -> msg.contains(config.recoveryMessage()))); 51 | } 52 | 53 | @Test 54 | void doesNotRecoverPlayerNotOnQueueServer() { 55 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 56 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 57 | QueueGroup group = QueueTestUtils.defaultGroup(config); 58 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 59 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 60 | QueueRecoveryHandler recoveryHandler = new QueueRecoveryHandler(environment); 61 | 62 | QueueType type = QueueTestUtils.defaultQueueType(config); 63 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("NotRecover"); 64 | player.setCurrentServer("otherServer"); 65 | 66 | recoveryHandler.recoverPlayer(player); 67 | 68 | assertTrue(type.getQueueMap().isEmpty()); 69 | assertTrue(player.getMessages().isEmpty()); 70 | } 71 | 72 | @Test 73 | void doesNotRecoverPlayerAlreadyInActiveTransfer() { 74 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 75 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 76 | QueueGroup group = QueueTestUtils.defaultGroup(config); 77 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 78 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 79 | QueueRecoveryHandler recoveryHandler = new QueueRecoveryHandler(environment); 80 | 81 | QueueType type = QueueTestUtils.defaultQueueType(config); 82 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("Transferring"); 83 | player.setCurrentServer(group.getQueueServer()); 84 | type.getActiveTransfers().add(player.getUniqueId()); 85 | 86 | recoveryHandler.recoverPlayer(player); 87 | 88 | assertTrue(type.getQueueMap().isEmpty()); 89 | } 90 | 91 | @Test 92 | void doesNotDuplicateRecoveryEntries() { 93 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 94 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 95 | QueueGroup group = QueueTestUtils.defaultGroup(config); 96 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer(), group.getTargetServers().getFirst()); 97 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 98 | QueueRecoveryHandler recoveryHandler = new QueueRecoveryHandler(environment); 99 | 100 | QueueType type = QueueTestUtils.defaultQueueType(config); 101 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("Duplicate"); 102 | player.setCurrentServer(group.getQueueServer()); 103 | type.getQueueMap().put(player.getUniqueId(), new QueueType.QueuedPlayer("target", QueueReason.RECOVERY)); 104 | 105 | recoveryHandler.recoverPlayer(player); 106 | 107 | assertEquals(1, type.getQueueMap().size()); 108 | // Should not send duplicate messages - player is already queued so no message sent 109 | long recoveryMessageCount = player.getMessages().stream() 110 | .filter(msg -> msg.contains(config.recoveryMessage())) 111 | .count(); 112 | assertEquals(0, recoveryMessageCount); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version to release' 8 | required: true 9 | after-version: 10 | description: 'Snapshot version after release' 11 | required: true 12 | 13 | jobs: 14 | set-release-version: 15 | uses: AlexProgrammerDE/PistonQueue/.github/workflows/set-version.yml@main 16 | with: 17 | version: ${{ inputs.version }} 18 | secrets: inherit 19 | 20 | build: 21 | needs: set-release-version 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: write 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v6 28 | with: 29 | ref: ${{ github.ref }} 30 | - name: Validate Gradle wrapper 31 | uses: gradle/actions/wrapper-validation@v5 32 | - name: Set up JDK 21 33 | uses: actions/setup-java@v5 34 | with: 35 | java-version: '21' 36 | distribution: 'temurin' 37 | - name: Setup Gradle 38 | uses: gradle/actions/setup-gradle@v5 39 | - name: Build with Gradle 40 | run: ./gradlew build test --stacktrace --scan 41 | 42 | - name: Build Changelog 43 | id: github_release 44 | uses: mikepenz/release-changelog-builder-action@v6 45 | with: 46 | mode: COMMIT 47 | toTag: ${{ github.ref }} 48 | configurationJson: | 49 | { 50 | "template": "#{{CHANGELOG}}", 51 | "commit_template": "- [`#{{SHORT_MERGE_SHA}}`](https://github.com/AlexProgrammerDE/PistonQueue/commit/#{{MERGE_SHA}}) #{{TITLE}}", 52 | "categories": [ 53 | { 54 | "title": "## 🚀 Features", 55 | "labels": ["feat", "feature"] 56 | }, 57 | { 58 | "title": "## 🐛 Fixes", 59 | "labels": ["fix", "bug"] 60 | }, 61 | { 62 | "title": "## 🏎️ Performance", 63 | "labels": ["perf"] 64 | }, 65 | { 66 | "title": "## 🏗 Refactor", 67 | "labels": ["refactor"] 68 | }, 69 | { 70 | "title": "## 📝 Documentation", 71 | "labels": ["docs"] 72 | }, 73 | { 74 | "title": "## 🔨 Build", 75 | "labels": ["build", "chore", "ci"] 76 | }, 77 | { 78 | "title": "## 💅 Style", 79 | "labels": ["style"] 80 | }, 81 | { 82 | "title": "## 🧪 Tests", 83 | "labels": ["test"] 84 | }, 85 | { 86 | "title": "## 💬 Other", 87 | "labels": [] 88 | }, 89 | { 90 | "title": "## 📦 Dependencies", 91 | "labels": ["dependencies"] 92 | } 93 | ], 94 | "empty_template": "no changes", 95 | "label_extractor": [ 96 | { 97 | "pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)", 98 | "on_property": "title", 99 | "target": "$1" 100 | } 101 | ], 102 | "custom_placeholders": [ 103 | { 104 | "name": "SHORT_MERGE_SHA", 105 | "source": "MERGE_SHA", 106 | "transformer": { 107 | "pattern": "^([0-9a-f]{7})[0-9a-f]*$", 108 | "target": "$1" 109 | } 110 | } 111 | ] 112 | } 113 | env: 114 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 115 | 116 | - uses: Kir-Antipov/mc-publish@v3.3 117 | with: 118 | modrinth-id: pistonqueue 119 | modrinth-featured: true 120 | modrinth-unfeature-mode: subset 121 | modrinth-token: ${{ secrets.MODRINTH_TOKEN }} 122 | 123 | github-tag: ${{ inputs.version }} 124 | github-generate-changelog: false 125 | github-draft: false 126 | github-prerelease: false 127 | github-commitish: main 128 | github-token: ${{ secrets.GITHUB_TOKEN }} 129 | 130 | files: | 131 | build/libs/PistonQueue-${{ inputs.version }}.jar 132 | build/libs/PistonQueue-Placeholder-${{ inputs.version }}.jar 133 | 134 | name: PistonQueue ${{ inputs.version }} 135 | version: ${{ inputs.version }} 136 | version-type: release 137 | changelog: ${{ steps.github_release.outputs.changelog }} 138 | 139 | github-changelog: | 140 | [![modrinth](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/modrinth_vector.svg)](https://modrinth.com/plugin/pistonqueue/version/${{ inputs.version }}) 141 | 142 | ${{ steps.github_release.outputs.changelog }} 143 | 144 | loaders: | 145 | bukkit 146 | bungeecord 147 | folia 148 | paper 149 | purpur 150 | spigot 151 | velocity 152 | waterfall 153 | game-versions: | 154 | >=1.8.0 155 | game-version-filter: releases 156 | java: | 157 | 21 158 | 159 | retry-attempts: 2 160 | retry-delay: 10000 161 | fail-mode: fail 162 | 163 | - name: Discord Webhook Action 164 | uses: tsickert/discord-webhook@v7.0.0 165 | with: 166 | webhook-url: ${{ secrets.WEBHOOK_URL }} 167 | content: <@&850705047938793503> New PistonQueue version released! 168 | embed-title: PistonQueue ${{ inputs.version }} 169 | embed-description: PistonQueue ${{ inputs.version }} has been released! Changelog and download can be found at https://modrinth.com/plugin/pistonqueue/version/${{ inputs.version }} 170 | embed-color: 16641028 171 | embed-thumbnail-url: https://raw.githubusercontent.com/AlexProgrammerDE/PistonQueue/refs/heads/main/images/logo.png 172 | 173 | set-after-version: 174 | needs: build 175 | uses: AlexProgrammerDE/PistonQueue/.github/workflows/set-version.yml@main 176 | with: 177 | version: ${{ inputs.after-version }} 178 | secrets: inherit 179 | -------------------------------------------------------------------------------- /shared/src/test/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueConnectorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.BanType; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 25 | import org.junit.jupiter.api.Test; 26 | 27 | import java.util.Set; 28 | import java.util.concurrent.ConcurrentHashMap; 29 | 30 | import static org.junit.jupiter.api.Assertions.*; 31 | 32 | class QueueConnectorTest { 33 | 34 | @Test 35 | void calculateEffectiveFreeSlotsReturnsZeroWhenNoSlots() { 36 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 37 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 38 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 39 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 40 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 41 | Set shadowBannedNames = ConcurrentHashMap.newKeySet(); 42 | ShadowBanService shadowBanService = shadowBannedNames::contains; 43 | QueueConnector connector = new QueueConnector(environment, calculator, shadowBanService); 44 | 45 | QueueType type = QueueTestUtils.defaultQueueType(config); 46 | type.getPlayersWithTypeInTarget().set(5); // Server is full 47 | 48 | int result = connector.calculateEffectiveFreeSlots(config, type); 49 | 50 | assertEquals(0, result); 51 | } 52 | 53 | @Test 54 | void calculateEffectiveFreeSlotsRespectsMaxPlayersPerMove() { 55 | Config config = QueueTestUtils.createConfigWithSingleQueueType(10); 56 | config.setMaxPlayersPerMove(2); 57 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 58 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 59 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 60 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 61 | Set shadowBannedNames = ConcurrentHashMap.newKeySet(); 62 | ShadowBanService shadowBanService = shadowBannedNames::contains; 63 | QueueConnector connector = new QueueConnector(environment, calculator, shadowBanService); 64 | 65 | QueueType type = QueueTestUtils.defaultQueueType(config); 66 | type.getPlayersWithTypeInTarget().set(0); // Server has slots 67 | 68 | int result = connector.calculateEffectiveFreeSlots(config, type); 69 | 70 | assertEquals(2, result); 71 | } 72 | 73 | @Test 74 | void shouldSkipPlayerDueToShadowBanReturnsTrueForLoopBan() { 75 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 76 | config.setShadowBanType(BanType.LOOP); 77 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 78 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 79 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 80 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 81 | Set shadowBannedNames = ConcurrentHashMap.newKeySet(); 82 | shadowBannedNames.add("BannedPlayer"); 83 | ShadowBanService shadowBanService = shadowBannedNames::contains; 84 | QueueConnector connector = new QueueConnector(environment, calculator, shadowBanService); 85 | 86 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("BannedPlayer"); 87 | 88 | boolean result = connector.shouldSkipPlayerDueToShadowBan(config, player); 89 | 90 | assertTrue(result); 91 | } 92 | 93 | @Test 94 | void shouldSkipPlayerDueToShadowBanReturnsFalseForNonBannedPlayer() { 95 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 96 | config.setShadowBanType(BanType.LOOP); 97 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 98 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 99 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 100 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 101 | Set shadowBannedNames = ConcurrentHashMap.newKeySet(); 102 | ShadowBanService shadowBanService = shadowBannedNames::contains; 103 | QueueConnector connector = new QueueConnector(environment, calculator, shadowBanService); 104 | 105 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("GoodPlayer"); 106 | 107 | boolean result = connector.shouldSkipPlayerDueToShadowBan(config, player); 108 | 109 | assertFalse(result); 110 | } 111 | 112 | @Test 113 | void preparePlayerForConnectionSendsMessagesAndResetsList() { 114 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 115 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 116 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 117 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 118 | QueueAvailabilityCalculator calculator = new QueueAvailabilityCalculator(); 119 | Set shadowBannedNames = ConcurrentHashMap.newKeySet(); 120 | ShadowBanService shadowBanService = shadowBannedNames::contains; 121 | QueueConnector connector = new QueueConnector(environment, calculator, shadowBanService); 122 | 123 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("Prepare"); 124 | 125 | connector.preparePlayerForConnection(config, player); 126 | 127 | assertTrue(player.getMessages().stream().anyMatch(msg -> msg.contains(config.joiningTargetServer()))); 128 | assertTrue(player.getPlayerListHeader().isEmpty() && player.getPlayerListFooter().isEmpty()); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /bukkit/src/main/java/net/pistonmaster/pistonqueue/bukkit/ServerListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.bukkit; 21 | 22 | import lombok.RequiredArgsConstructor; 23 | import org.bukkit.Bukkit; 24 | import org.bukkit.ChatColor; 25 | import org.bukkit.GameMode; 26 | import org.bukkit.Location; 27 | import org.bukkit.entity.HumanEntity; 28 | import org.bukkit.entity.Player; 29 | import org.bukkit.event.EventHandler; 30 | import org.bukkit.event.Listener; 31 | import org.bukkit.event.entity.EntityDamageByBlockEvent; 32 | import org.bukkit.event.entity.EntityDamageByEntityEvent; 33 | import org.bukkit.event.entity.EntityDamageEvent; 34 | import org.bukkit.event.entity.FoodLevelChangeEvent; 35 | import org.bukkit.event.player.*; 36 | import org.bukkit.scoreboard.Scoreboard; 37 | import org.bukkit.scoreboard.ScoreboardManager; 38 | import org.bukkit.scoreboard.Team; 39 | 40 | import java.util.Locale; 41 | import java.util.Objects; 42 | import java.util.concurrent.ThreadLocalRandom; 43 | import java.util.logging.Level; 44 | 45 | @RequiredArgsConstructor 46 | public final class ServerListener implements Listener { 47 | private final PistonQueueBukkit plugin; 48 | 49 | @EventHandler(ignoreCancelled = true) 50 | public void onPlayerJoin(PlayerJoinEvent event) { 51 | Player player = event.getPlayer(); 52 | 53 | if (isExcluded(player)) { 54 | player.sendMessage(ChatColor.GOLD + "Due to your permissions, you've been excluded from the queue restrictions."); 55 | 56 | return; 57 | } 58 | 59 | if (plugin.isForceGamemode()) { 60 | player.setGameMode(GameMode.valueOf(plugin.getForcedGamemode().toUpperCase(Locale.ROOT))); 61 | } 62 | 63 | if (plugin.isHidePlayers()) { 64 | plugin.getServer().getOnlinePlayers().forEach(onlinePlayer -> { 65 | player.hidePlayer(plugin, onlinePlayer); 66 | onlinePlayer.hidePlayer(plugin, event.getPlayer()); 67 | 68 | event.setJoinMessage(null); 69 | }); 70 | } 71 | 72 | ScoreboardManager manager = Bukkit.getScoreboardManager(); 73 | if (plugin.isTeam() && manager != null) { 74 | Scoreboard scoreboard = manager.getNewScoreboard(); 75 | 76 | Team team = scoreboard.registerNewTeam(plugin.getTeamName() 77 | .replace("%player_name%", player.getName()) 78 | .replace("%random%", String.valueOf(getRandomNumberUsingNextInt(-9999, 9999)))); 79 | team.setCanSeeFriendlyInvisibles(false); 80 | team.setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.NEVER); 81 | 82 | player.setScoreboard(scoreboard); 83 | 84 | Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> team.addEntry(player.getName()), 20L); 85 | } 86 | 87 | if (plugin.isForceLocation()) { 88 | player.teleport(Objects.requireNonNull(generateForcedLocation())); 89 | } 90 | 91 | if (plugin.isProtocolLib() && plugin.isDisableDebug()) { 92 | ProtocolLibWrapper.removeDebug(player); 93 | } 94 | } 95 | 96 | @EventHandler(ignoreCancelled = true) 97 | public void onPlayerQuit(PlayerQuitEvent event) { 98 | if (plugin.isHidePlayers()) { 99 | event.setQuitMessage(null); 100 | } 101 | } 102 | 103 | @EventHandler(ignoreCancelled = true) 104 | public void onPlayerRespawn(PlayerRespawnEvent event) { 105 | if (plugin.isForceLocation() && !isExcluded(event.getPlayer())) { 106 | event.setRespawnLocation(Objects.requireNonNull(generateForcedLocation())); 107 | } 108 | } 109 | 110 | @EventHandler(ignoreCancelled = true) 111 | public void onPlayerHunger(FoodLevelChangeEvent event) { 112 | if (plugin.isPreventHunger() && !isExcluded(event.getEntity())) { 113 | event.setCancelled(true); 114 | } 115 | } 116 | 117 | @EventHandler(ignoreCancelled = true) 118 | public void onPlayerHunger(PlayerExpChangeEvent event) { 119 | if (plugin.isPreventHunger() && !isExcluded(event.getPlayer())) { 120 | event.setAmount(0); 121 | } 122 | } 123 | 124 | @EventHandler(ignoreCancelled = true) 125 | public void onPlayerDamage(EntityDamageEvent event) { 126 | if (!(event.getEntity() instanceof Player player)) { 127 | return; 128 | } 129 | 130 | if (plugin.isPreventDamage() && !isExcluded(player)) { 131 | event.setCancelled(true); 132 | } 133 | } 134 | 135 | @EventHandler(ignoreCancelled = true) 136 | public void onPlayerDamage(EntityDamageByBlockEvent event) { 137 | if (!(event.getEntity() instanceof Player player)) { 138 | return; 139 | } 140 | 141 | if (plugin.isPreventDamage() && !isExcluded(player)) { 142 | event.setCancelled(true); 143 | } 144 | } 145 | 146 | @EventHandler(ignoreCancelled = true) 147 | public void onPlayerDamage(EntityDamageByEntityEvent event) { 148 | if (!(event.getEntity() instanceof Player player)) { 149 | return; 150 | } 151 | 152 | if (plugin.isPreventDamage() && !isExcluded(player)) { 153 | event.setCancelled(true); 154 | } 155 | } 156 | 157 | @EventHandler(ignoreCancelled = true) 158 | public void onChat(AsyncPlayerChatEvent event) { 159 | if (plugin.isDisableChat()) { 160 | event.setCancelled(true); 161 | } 162 | } 163 | 164 | @EventHandler(ignoreCancelled = true) 165 | public void onCmd(PlayerCommandPreprocessEvent event) { 166 | if (plugin.isDisableCmd()) { 167 | event.setCancelled(true); 168 | } 169 | } 170 | 171 | @EventHandler(ignoreCancelled = true) 172 | public void onMove(PlayerMoveEvent event) { 173 | if (plugin.isRestrictMovement() && !isExcluded(event.getPlayer())) { 174 | event.setCancelled(true); 175 | } 176 | } 177 | 178 | private boolean isExcluded(HumanEntity player) { 179 | return player.hasPermission("queue.admin"); 180 | } 181 | 182 | private Location generateForcedLocation() { 183 | if (plugin.getServer().getWorld(plugin.getForcedWorldName()) == null) { 184 | plugin.getLogger().log(Level.SEVERE, "Invalid forcedWorldName!! Check the configuration."); 185 | 186 | return null; 187 | } 188 | 189 | return new Location(plugin.getServer().getWorld(plugin.getForcedWorldName()), plugin.getForcedX(), plugin.getForcedY(), plugin.getForcedZ()); 190 | } 191 | 192 | public int getRandomNumberUsingNextInt(int min, int max) { 193 | return ThreadLocalRandom.current().nextInt(min, max); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /shared/src/test/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueEntryFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 24 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 25 | import net.pistonmaster.pistonqueue.shared.queue.QueueType.QueueReason; 26 | import org.junit.jupiter.api.Test; 27 | 28 | import java.util.List; 29 | import java.util.Set; 30 | 31 | import static org.junit.jupiter.api.Assertions.*; 32 | 33 | class QueueEntryFactoryTest { 34 | 35 | @Test 36 | void enqueuesPlayerWithCorrectTarget() { 37 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 38 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 39 | QueueGroup group = QueueTestUtils.defaultGroup(config); 40 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 41 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 42 | QueueEntryFactory entryFactory = new QueueEntryFactory(environment); 43 | 44 | QueueType type = QueueTestUtils.defaultQueueType(config); 45 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("Enqueue"); 46 | QueueTestUtils.TestPreConnectEvent event = QueueTestUtils.preConnectEvent(player, "customTarget"); 47 | 48 | entryFactory.enqueue(player, group, type, event, true, config); 49 | 50 | assertEquals(group.getQueueServer(), event.getTarget().orElseThrow()); 51 | assertTrue(type.getQueueMap().containsKey(player.getUniqueId())); 52 | assertEquals("customTarget", type.getQueueMap().get(player.getUniqueId()).targetServer()); 53 | } 54 | 55 | @Test 56 | void forcesDefaultTargetWhenConfigured() { 57 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 58 | config.setForceTargetServer(true); 59 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 60 | QueueGroup group = QueueTestUtils.defaultGroup(config); 61 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer(), group.getTargetServers().getFirst()); 62 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 63 | QueueEntryFactory entryFactory = new QueueEntryFactory(environment); 64 | 65 | QueueType type = QueueTestUtils.defaultQueueType(config); 66 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("Force"); 67 | QueueTestUtils.TestPreConnectEvent event = QueueTestUtils.preConnectEvent(player, "customTarget"); 68 | 69 | entryFactory.enqueue(player, group, type, event, true, config); 70 | 71 | assertEquals(group.getTargetServers().getFirst(), type.getQueueMap().get(player.getUniqueId()).targetServer()); 72 | } 73 | 74 | @Test 75 | void sendsFullMessageOnlyOnce() { 76 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 77 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 78 | QueueGroup group = QueueTestUtils.defaultGroup(config); 79 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 80 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 81 | QueueEntryFactory entryFactory = new QueueEntryFactory(environment); 82 | 83 | QueueType type = QueueTestUtils.defaultQueueType(config); 84 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("Full"); 85 | QueueTestUtils.TestPreConnectEvent event = QueueTestUtils.preConnectEvent(player, "target"); 86 | 87 | entryFactory.enqueue(player, group, type, event, true, config); 88 | entryFactory.enqueue(player, group, type, event, true, config); // Try to enqueue again 89 | 90 | long fullMessageCount = player.getMessages().stream() 91 | .filter(msg -> msg.contains(config.serverIsFullMessage())) 92 | .count(); 93 | assertEquals(1, fullMessageCount); 94 | } 95 | 96 | @Test 97 | void setsPlayerListHeaderAndFooter() { 98 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 99 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 100 | QueueGroup group = QueueTestUtils.defaultGroup(config); 101 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 102 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 103 | QueueEntryFactory entryFactory = new QueueEntryFactory(environment); 104 | 105 | QueueType type = QueueTestUtils.defaultQueueType(config); 106 | type.setHeader(List.of("Header Line 1", "Header Line 2")); 107 | type.setFooter(List.of("Footer Line 1")); 108 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("List"); 109 | QueueTestUtils.TestPreConnectEvent event = QueueTestUtils.preConnectEvent(player, "target"); 110 | 111 | entryFactory.enqueue(player, group, type, event, false, config); 112 | 113 | assertEquals(List.of("Header Line 1", "Header Line 2"), player.getPlayerListHeader()); 114 | assertEquals(List.of("Footer Line 1"), player.getPlayerListFooter()); 115 | } 116 | 117 | @Test 118 | void doesNotSendFullMessageWhenNotServerFull() { 119 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 120 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 121 | QueueGroup group = QueueTestUtils.defaultGroup(config); 122 | Set onlineServers = QueueTestUtils.onlineServers(group.getQueueServer()); 123 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 124 | QueueEntryFactory entryFactory = new QueueEntryFactory(environment); 125 | 126 | QueueType type = QueueTestUtils.defaultQueueType(config); 127 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("NotFull"); 128 | QueueTestUtils.TestPreConnectEvent event = QueueTestUtils.preConnectEvent(player, "target"); 129 | 130 | entryFactory.enqueue(player, group, type, event, false, config); 131 | 132 | assertTrue(player.getMessages().stream() 133 | .noneMatch(msg -> msg.contains(config.serverIsFullMessage()))); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/utils/StorageTool.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.utils; 21 | 22 | import de.exlll.configlib.YamlConfigurations; 23 | import net.pistonmaster.pistonqueue.shared.config.StorageData; 24 | 25 | import java.io.IOException; 26 | import java.nio.file.Files; 27 | import java.nio.file.Path; 28 | import java.text.ParseException; 29 | import java.text.SimpleDateFormat; 30 | import java.time.Instant; 31 | import java.time.format.DateTimeParseException; 32 | import java.util.Date; 33 | import java.util.LinkedHashMap; 34 | import java.util.List; 35 | import java.util.Locale; 36 | import java.util.Map; 37 | import java.util.logging.Level; 38 | import java.util.logging.Logger; 39 | 40 | public final class StorageTool { 41 | private static final Logger LOGGER = Logger.getLogger(StorageTool.class.getName()); 42 | private static Path dataDirectory; 43 | private static StorageData dataConfig; 44 | private static Path dataFile; 45 | 46 | private StorageTool() { 47 | } 48 | 49 | /** 50 | * Shadow-ban a player! 51 | * 52 | * @param playerName The player to shadow-ban. 53 | * @param date The date when he will be unbanned. 54 | * @return true if player got shadow-banned and if already shadow-banned false. 55 | */ 56 | public static boolean shadowBanPlayer(String playerName, Date date) { 57 | playerName = playerName.toLowerCase(Locale.ROOT); 58 | manageBan(playerName); 59 | 60 | Map bans = dataConfig.getMutableBans(); 61 | if (!bans.containsKey(playerName)) { 62 | bans.put(playerName, date.toInstant().toString()); 63 | saveData(); 64 | 65 | return true; 66 | } else { 67 | return false; 68 | } 69 | } 70 | 71 | /** 72 | * Un-shadow-ban a player! 73 | * 74 | * @param playerName The player to un-shadow-ban. 75 | * @return true if a player got un-shadow-banned and false if he wasn't shadow-banned. 76 | */ 77 | public static boolean unShadowBanPlayer(String playerName) { 78 | playerName = playerName.toLowerCase(Locale.ROOT); 79 | if (dataConfig.getMutableBans().remove(playerName) != null) { 80 | saveData(); 81 | 82 | return true; 83 | } else { 84 | return false; 85 | } 86 | } 87 | 88 | public static boolean isShadowBanned(String playerName) { 89 | playerName = playerName.toLowerCase(Locale.ROOT); 90 | manageBan(playerName); 91 | 92 | return dataConfig.getMutableBans().containsKey(playerName); 93 | } 94 | 95 | private static void manageBan(String playerName) { 96 | playerName = playerName.toLowerCase(Locale.ROOT); 97 | Instant now = Instant.now(); 98 | 99 | if (dataConfig.getMutableBans().containsKey(playerName)) { 100 | Instant expiresAt = parseExpiryInstant(dataConfig.getMutableBans().get(playerName)); 101 | if (expiresAt != null && !now.isBefore(expiresAt)) { 102 | unShadowBanPlayer(playerName); 103 | } 104 | } 105 | } 106 | 107 | private static void loadData() { 108 | generateFile(); 109 | convertLegacyDataIfNeeded(); 110 | ensureFileInitialized(); 111 | dataConfig = YamlConfigurations.update(dataFile, StorageData.class); 112 | } 113 | 114 | private static void saveData() { 115 | generateFile(); 116 | YamlConfigurations.save(dataFile, StorageData.class, dataConfig); 117 | } 118 | 119 | private static void generateFile() { 120 | try { 121 | if (!Files.exists(dataDirectory)) { 122 | Files.createDirectories(dataDirectory); 123 | } 124 | 125 | if (!Files.exists(dataFile)) { 126 | Files.createFile(dataFile); 127 | } 128 | } catch (IOException e) { 129 | LOGGER.log(Level.SEVERE, "Failed to create data file", e); 130 | } 131 | } 132 | 133 | public static void setupTool(Path dataDirectory) { 134 | StorageTool.dataDirectory = dataDirectory; 135 | StorageTool.dataFile = dataDirectory.resolve("data.yml"); 136 | 137 | loadData(); 138 | } 139 | 140 | private static void convertLegacyDataIfNeeded() { 141 | try { 142 | if (!Files.exists(dataFile)) { 143 | return; 144 | } 145 | List lines = Files.readAllLines(dataFile); 146 | boolean alreadyNewFormat = lines.stream() 147 | .map(String::trim) 148 | .anyMatch(line -> line.startsWith("bans:")); 149 | if (alreadyNewFormat || lines.isEmpty()) { 150 | return; 151 | } 152 | 153 | Map legacyEntries = new LinkedHashMap<>(); 154 | for (String rawLine : lines) { 155 | String line = rawLine.trim(); 156 | if (line.isEmpty() || line.startsWith("#")) { 157 | continue; 158 | } 159 | int colonIndex = line.indexOf(':'); 160 | if (colonIndex < 0) { 161 | continue; 162 | } 163 | String key = line.substring(0, colonIndex).trim(); 164 | String value = line.substring(colonIndex + 1).trim(); 165 | if (value.startsWith("'") && value.endsWith("'") && value.length() >= 2) { 166 | value = value.substring(1, value.length() - 1); 167 | } else if (value.startsWith("\"") && value.endsWith("\"") && value.length() >= 2) { 168 | value = value.substring(1, value.length() - 1); 169 | } 170 | if (!key.isEmpty()) { 171 | legacyEntries.put(key, value); 172 | } 173 | } 174 | 175 | if (legacyEntries.isEmpty()) { 176 | return; 177 | } 178 | 179 | StorageData legacyData = new StorageData(); 180 | legacyData.getMutableBans().putAll(legacyEntries); 181 | YamlConfigurations.save(dataFile, StorageData.class, legacyData); 182 | } catch (IOException e) { 183 | LOGGER.log(Level.SEVERE, "Failed to convert legacy data", e); 184 | } 185 | } 186 | 187 | private static void ensureFileInitialized() { 188 | try { 189 | if (Files.size(dataFile) == 0) { 190 | YamlConfigurations.save(dataFile, StorageData.class, new StorageData()); 191 | } 192 | } catch (IOException e) { 193 | LOGGER.log(Level.SEVERE, "Failed to initialize storage file", e); 194 | } 195 | } 196 | 197 | private static Instant parseExpiryInstant(String value) { 198 | if (value == null) { 199 | return null; 200 | } 201 | try { 202 | return Instant.parse(value); 203 | } catch (DateTimeParseException ignored) { 204 | try { 205 | SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy", Locale.of("en")); 206 | Date parsed = sdf.parse(value); 207 | return parsed.toInstant(); 208 | } catch (ParseException e) { 209 | LOGGER.log(Level.WARNING, "Failed to parse shadow ban expiry value \"" + value + '"', e); 210 | return null; 211 | } 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /shared/src/main/java/net/pistonmaster/pistonqueue/shared/queue/logic/QueueConnector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import com.google.common.io.ByteArrayDataOutput; 23 | import com.google.common.io.ByteStreams; 24 | import net.pistonmaster.pistonqueue.shared.config.Config; 25 | import net.pistonmaster.pistonqueue.shared.queue.BanType; 26 | import net.pistonmaster.pistonqueue.shared.queue.QueueGroup; 27 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 28 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 29 | 30 | import java.time.Duration; 31 | import java.time.Instant; 32 | import java.util.AbstractMap; 33 | import java.util.ArrayList; 34 | import java.util.Collections; 35 | import java.util.HashMap; 36 | import java.util.HashSet; 37 | import java.util.Iterator; 38 | import java.util.List; 39 | import java.util.Map; 40 | import java.util.Objects; 41 | import java.util.Optional; 42 | import java.util.Set; 43 | import java.util.UUID; 44 | import java.util.concurrent.ThreadLocalRandom; 45 | import java.util.concurrent.locks.Lock; 46 | 47 | /** 48 | * Responsible for moving players from the queue onto their target server. 49 | */ 50 | public final class QueueConnector { 51 | private final QueueEnvironment environment; 52 | private final QueueAvailabilityCalculator availabilityCalculator; 53 | private final ShadowBanService shadowBanService; 54 | 55 | public QueueConnector(QueueEnvironment environment, QueueAvailabilityCalculator availabilityCalculator, ShadowBanService shadowBanService) { 56 | this.environment = Objects.requireNonNull(environment, "environment"); 57 | this.availabilityCalculator = Objects.requireNonNull(availabilityCalculator, "availabilityCalculator"); 58 | this.shadowBanService = Objects.requireNonNull(shadowBanService, "shadowBanService"); 59 | } 60 | 61 | public void connectPlayers(QueueGroup group, QueueType type) { 62 | Config config = environment.config(); 63 | int freeSlots = calculateEffectiveFreeSlots(config, type); 64 | 65 | if (freeSlots <= 0) { 66 | return; 67 | } 68 | 69 | int movesLeft = freeSlots; 70 | Set processedThisCycle = new HashSet<>(); 71 | 72 | while (movesLeft > 0) { 73 | Map.Entry entry = pollNextQueueEntry(type); 74 | if (entry == null) { 75 | break; 76 | } 77 | 78 | if (!processedThisCycle.add(entry.getKey())) { 79 | requeuePlayer(type, entry); 80 | break; 81 | } 82 | 83 | Optional optional = environment.plugin().getPlayer(entry.getKey()); 84 | if (optional.isEmpty()) { 85 | type.getActiveTransfers().remove(entry.getKey()); 86 | continue; 87 | } 88 | PlayerWrapper player = optional.get(); 89 | 90 | if (shouldSkipPlayerDueToShadowBan(config, player)) { 91 | player.sendMessage(config.shadowBanMessage()); 92 | requeuePlayer(type, entry); 93 | continue; 94 | } 95 | 96 | preparePlayerForConnection(config, player); 97 | recordPositionDuration(type, entry.getKey()); 98 | connectPlayer(player, entry.getValue().targetServer()); 99 | type.getActiveTransfers().remove(entry.getKey()); 100 | 101 | movesLeft--; 102 | } 103 | 104 | if (config.sendXpSound()) { 105 | sendXPSoundToQueueType(group, type); 106 | } 107 | } 108 | 109 | int calculateEffectiveFreeSlots(Config config, QueueType type) { 110 | int freeSlots = availabilityCalculator.getFreeSlots(type); 111 | if (freeSlots <= 0) { 112 | return 0; 113 | } 114 | return Math.min(freeSlots, config.maxPlayersPerMove()); 115 | } 116 | 117 | boolean shouldSkipPlayerDueToShadowBan(Config config, PlayerWrapper player) { 118 | if (!shadowBanService.isShadowBanned(player.getName())) { 119 | return false; 120 | } 121 | 122 | return config.shadowBanType() == BanType.LOOP 123 | || (config.shadowBanType() == BanType.PERCENT && ThreadLocalRandom.current().nextInt(100) >= config.shadowBanPercent()); 124 | } 125 | 126 | void preparePlayerForConnection(Config config, PlayerWrapper player) { 127 | player.sendMessage(config.joiningTargetServer()); 128 | player.resetPlayerList(); 129 | } 130 | 131 | void recordPositionDuration(QueueType type, UUID playerId) { 132 | indexPositionTime(type); 133 | 134 | Map cache = type.getPositionCache().get(playerId); 135 | if (cache != null) { 136 | Lock durationWriteLock = type.getDurationLock().writeLock(); 137 | durationWriteLock.lock(); 138 | try { 139 | cache.forEach((position, instant) -> 140 | type.getDurationFromPosition().put(position, Duration.between(instant, Instant.now()))); 141 | } finally { 142 | durationWriteLock.unlock(); 143 | } 144 | } 145 | } 146 | 147 | void connectPlayer(PlayerWrapper player, String targetServer) { 148 | player.connect(targetServer); 149 | } 150 | 151 | private void sendXPSoundToQueueType(QueueGroup group, QueueType type) { 152 | ByteArrayDataOutput out = ByteStreams.newDataOutput(); 153 | out.writeUTF("xpV2"); 154 | 155 | List uuids = new ArrayList<>(5); 156 | Lock readLock = type.getQueueLock().readLock(); 157 | readLock.lock(); 158 | try { 159 | for (UUID uuid : type.getQueueMap().keySet()) { 160 | uuids.add(uuid); 161 | if (uuids.size() == 5) { 162 | break; 163 | } 164 | } 165 | } finally { 166 | readLock.unlock(); 167 | } 168 | 169 | out.writeInt(uuids.size()); 170 | uuids.forEach(id -> out.writeUTF(id.toString())); 171 | 172 | environment.plugin().getServer(group.getQueueServer()).ifPresent(server -> 173 | server.sendPluginMessage("piston:queue", out.toByteArray())); 174 | } 175 | 176 | private void indexPositionTime(QueueType type) { 177 | int position = 0; 178 | 179 | Lock readLock = type.getQueueLock().readLock(); 180 | readLock.lock(); 181 | try { 182 | for (UUID uuid : type.getQueueMap().keySet()) { 183 | position++; 184 | Map list = type.getPositionCache().get(uuid); 185 | if (list == null) { 186 | type.getPositionCache().put(uuid, new HashMap<>(Collections.singletonMap(position, Instant.now()))); 187 | } else if (!list.containsKey(position)) { 188 | list.put(position, Instant.now()); 189 | } 190 | } 191 | } finally { 192 | readLock.unlock(); 193 | } 194 | } 195 | 196 | private Map.Entry pollNextQueueEntry(QueueType type) { 197 | Lock writeLock = type.getQueueLock().writeLock(); 198 | writeLock.lock(); 199 | try { 200 | Iterator> iterator = type.getQueueMap().entrySet().iterator(); 201 | if (!iterator.hasNext()) { 202 | return null; 203 | } 204 | 205 | Map.Entry entry = iterator.next(); 206 | iterator.remove(); 207 | type.getActiveTransfers().add(entry.getKey()); 208 | return new AbstractMap.SimpleEntry<>(entry); 209 | } finally { 210 | writeLock.unlock(); 211 | } 212 | } 213 | 214 | private void requeuePlayer(QueueType type, Map.Entry entry) { 215 | Lock writeLock = type.getQueueLock().writeLock(); 216 | writeLock.lock(); 217 | try { 218 | type.getActiveTransfers().remove(entry.getKey()); 219 | type.getQueueMap().put(entry.getKey(), entry.getValue()); 220 | } finally { 221 | writeLock.unlock(); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /bungee/src/main/java/net/pistonmaster/pistonqueue/bungee/PistonQueueBungee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.bungee; 21 | 22 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 23 | import lombok.Getter; 24 | import net.md_5.bungee.api.ChatColor; 25 | import net.md_5.bungee.api.ChatMessageType; 26 | import net.md_5.bungee.api.config.ServerInfo; 27 | import net.md_5.bungee.api.connection.ProxiedPlayer; 28 | import net.md_5.bungee.api.plugin.Plugin; 29 | import net.md_5.bungee.api.plugin.PluginManager; 30 | import net.pistonmaster.pistonqueue.bungee.commands.MainCommand; 31 | import net.pistonmaster.pistonqueue.bungee.listeners.QueueListenerBungee; 32 | import net.pistonmaster.pistonqueue.bungee.utils.ChatUtils; 33 | import net.pistonmaster.pistonqueue.shared.chat.MessageType; 34 | import net.pistonmaster.pistonqueue.shared.config.Config; 35 | import net.pistonmaster.pistonqueue.shared.hooks.PistonMOTDPlaceholder; 36 | import net.pistonmaster.pistonqueue.shared.plugin.PistonQueuePlugin; 37 | import net.pistonmaster.pistonqueue.shared.utils.StorageTool; 38 | import net.pistonmaster.pistonqueue.shared.wrapper.PlayerWrapper; 39 | import net.pistonmaster.pistonqueue.shared.wrapper.ServerInfoWrapper; 40 | import net.pistonmaster.pistonutils.update.GitHubUpdateChecker; 41 | import net.pistonmaster.pistonutils.update.SemanticVersion; 42 | import org.bstats.bungeecord.Metrics; 43 | 44 | import java.io.IOException; 45 | import java.nio.file.Path; 46 | import java.util.Collections; 47 | import java.util.List; 48 | import java.util.Optional; 49 | import java.util.UUID; 50 | import java.util.concurrent.CompletableFuture; 51 | import java.util.concurrent.TimeUnit; 52 | import java.util.stream.Collectors; 53 | import java.util.logging.Level; 54 | 55 | @Getter 56 | public final class PistonQueueBungee extends Plugin implements PistonQueuePlugin { 57 | private final Config configuration = new Config(); 58 | private final QueueListenerBungee queueListenerBungee = new QueueListenerBungee(this); 59 | 60 | @Override 61 | public void onEnable() { 62 | PluginManager manager = getProxy().getPluginManager(); 63 | 64 | info(ChatColor.BLUE + "Loading config"); 65 | processConfig(getDataDirectory()); 66 | 67 | StorageTool.setupTool(getDataDirectory()); 68 | initializeReservationSlots(); 69 | 70 | info(ChatColor.BLUE + "Looking for hooks"); 71 | if (getProxy().getPluginManager().getPlugin("PistonMOTD") != null) { 72 | info(ChatColor.BLUE + "Hooking into PistonMOTD"); 73 | new PistonMOTDPlaceholder(configuration); 74 | } 75 | 76 | info(ChatColor.BLUE + "Registering plugin messaging channel"); 77 | getProxy().registerChannel("piston:queue"); 78 | 79 | info(ChatColor.BLUE + "Registering commands"); 80 | manager.registerCommand(this, new MainCommand(this)); 81 | 82 | info(ChatColor.BLUE + "Registering listeners"); 83 | manager.registerListener(this, queueListenerBungee); 84 | 85 | info(ChatColor.BLUE + "Loading Metrics"); 86 | new Metrics(this, 8755); 87 | 88 | info(ChatColor.BLUE + "Checking for update"); 89 | try { 90 | String currentVersionString = this.getDescription().getVersion(); 91 | SemanticVersion gitHubVersion = new GitHubUpdateChecker() 92 | .getVersion("https://api.github.com/repos/AlexProgrammerDE/PistonQueue/releases/latest"); 93 | SemanticVersion currentVersion = SemanticVersion.fromString(currentVersionString); 94 | 95 | if (gitHubVersion.isNewerThan(currentVersion)) { 96 | info(ChatColor.RED + "There is an update available!"); 97 | info(ChatColor.RED + "Current version: " + currentVersionString + " New version: " + gitHubVersion); 98 | info(ChatColor.RED + "Download it at: https://modrinth.com/plugin/pistonqueue"); 99 | } else { 100 | info(ChatColor.BLUE + "You're up to date!"); 101 | } 102 | } catch (IOException e) { 103 | error("Could not check for updates!"); 104 | getLogger().log(Level.SEVERE, "Update check failed", e); 105 | } 106 | 107 | info(ChatColor.BLUE + "Scheduling tasks"); 108 | scheduleTasks(queueListenerBungee); 109 | } 110 | 111 | @Override 112 | public Optional getPlayer(UUID uuid) { 113 | return Optional.ofNullable(getProxy().getPlayer(uuid)).map(this::wrapPlayer); 114 | } 115 | 116 | @Override 117 | public List getPlayers() { 118 | return getProxy().getPlayers().stream().map(this::wrapPlayer).collect(Collectors.toList()); 119 | } 120 | 121 | @Override 122 | public Optional getServer(String name) { 123 | return Optional.ofNullable(getProxy().getServerInfo(name)).map(this::wrapServer); 124 | } 125 | 126 | @Override 127 | public void schedule(Runnable runnable, long delay, long period, TimeUnit unit) { 128 | getProxy().getScheduler().schedule(this, runnable, delay, period, unit); 129 | } 130 | 131 | @Override 132 | public void info(String message) { 133 | getLogger().info(message); 134 | } 135 | 136 | @Override 137 | public void warning(String message) { 138 | getLogger().warning(message); 139 | } 140 | 141 | @Override 142 | public void error(String message) { 143 | getLogger().severe(message); 144 | } 145 | 146 | @Override 147 | public List getAuthors() { 148 | return Collections.singletonList(getDescription().getAuthor()); 149 | } 150 | 151 | @Override 152 | public String getVersion() { 153 | return getDescription().getVersion(); 154 | } 155 | 156 | @Override 157 | public Path getDataDirectory() { 158 | return getDataFolder().toPath(); 159 | } 160 | 161 | @Override 162 | @SuppressFBWarnings( 163 | value = "EI_EXPOSE_REP", 164 | justification = "Configuration is intentionally shared and mutated via copyFrom to keep references in sync" 165 | ) 166 | public Config getConfiguration() { 167 | return configuration; 168 | } 169 | 170 | private ServerInfoWrapper wrapServer(ServerInfo serverInfo) { 171 | return new ServerInfoWrapper() { 172 | @Override 173 | public List getConnectedPlayers() { 174 | return serverInfo.getPlayers().stream().map(PistonQueueBungee.this::wrapPlayer).collect(Collectors.toList()); 175 | } 176 | 177 | @Override 178 | public boolean isOnline() { 179 | CompletableFuture future = new CompletableFuture<>(); 180 | serverInfo.ping((result, error) -> future.complete(error == null && result != null)); 181 | return future.join(); 182 | } 183 | 184 | @Override 185 | public void sendPluginMessage(String channel, byte[] data) { 186 | serverInfo.sendData("piston:queue", data); 187 | } 188 | }; 189 | } 190 | 191 | public PlayerWrapper wrapPlayer(ProxiedPlayer player) { 192 | return new PlayerWrapper() { 193 | @Override 194 | public boolean hasPermission(String node) { 195 | return player.hasPermission(node); 196 | } 197 | 198 | @Override 199 | public void connect(String server) { 200 | Optional optional = Optional.ofNullable(getProxy().getServerInfo(server)); 201 | 202 | if (optional.isEmpty()) { 203 | error("Server" + server + " not found!!!"); 204 | return; 205 | } 206 | 207 | player.connect(optional.get()); 208 | } 209 | 210 | @Override 211 | public Optional getCurrentServer() { 212 | return Optional.ofNullable(player.getServer()).map(server -> server.getInfo().getName()); 213 | } 214 | 215 | @Override 216 | public void sendMessage(MessageType type, String message) { 217 | if ("/".equalsIgnoreCase(message) || message.isBlank()) { 218 | return; 219 | } 220 | 221 | switch (type) { 222 | case CHAT -> player.sendMessage(ChatMessageType.CHAT, ChatUtils.parseToComponent(configuration, message)); 223 | case ACTION_BAR -> player.sendMessage(ChatMessageType.ACTION_BAR, ChatUtils.parseToComponent(configuration, message)); 224 | } 225 | } 226 | 227 | @Override 228 | public void sendPlayerList(List header, List footer) { 229 | player.setTabHeader(ChatUtils.parseTab(configuration, header), ChatUtils.parseTab(configuration, footer)); 230 | } 231 | 232 | @Override 233 | public void resetPlayerList() { 234 | player.resetTabHeader(); 235 | } 236 | 237 | @Override 238 | public String getName() { 239 | return player.getName(); 240 | } 241 | 242 | @Override 243 | public UUID getUniqueId() { 244 | return player.getUniqueId(); 245 | } 246 | 247 | @Override 248 | public void disconnect(String message) { 249 | player.disconnect(ChatUtils.parseToComponent(configuration, message)); 250 | } 251 | }; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /shared/src/test/java/net/pistonmaster/pistonqueue/shared/queue/logic/KickEventHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * #%L 3 | * PistonQueue 4 | * %% 5 | * Copyright (C) 2021 AlexProgrammerDE 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package net.pistonmaster.pistonqueue.shared.queue.logic; 21 | 22 | import net.pistonmaster.pistonqueue.shared.config.Config; 23 | import net.pistonmaster.pistonqueue.shared.queue.QueueType; 24 | import org.junit.jupiter.api.Test; 25 | 26 | import java.util.List; 27 | import java.util.Set; 28 | 29 | import static org.junit.jupiter.api.Assertions.*; 30 | 31 | class KickEventHandlerTest { 32 | 33 | @Test 34 | void redirectsToQueueWhenKickedFromDownTargetServer() { 35 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 36 | config.setTargetServer("target"); 37 | config.setIfTargetDownSendToQueue(true); 38 | config.setDownWordList(List.of("server", "down", "offline")); 39 | config.setIfTargetDownSendToQueueMessage("Server is down, redirecting to queue"); 40 | 41 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 42 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 43 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 44 | KickEventHandler handler = new KickEventHandler(config, environment); 45 | 46 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("KickedPlayer"); 47 | QueueTestUtils.TestKickEvent event = QueueTestUtils.kickEvent(player, "target", "Server is currently down"); 48 | 49 | handler.handleKick(event); 50 | 51 | assertEquals("queue", event.getCancelServer().orElseThrow()); 52 | assertTrue(player.getMessages().stream().anyMatch(msg -> msg.contains("Server is down, redirecting to queue"))); 53 | assertTrue(QueueTestUtils.defaultQueueType(config).getQueueMap().containsKey(player.getUniqueId())); 54 | assertEquals("target", QueueTestUtils.defaultQueueType(config).getQueueMap().get(player.getUniqueId()).targetServer()); 55 | } 56 | 57 | @Test 58 | void doesNotRedirectWhenQueueRedirectionDisabled() { 59 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 60 | config.setTargetServer("target"); 61 | config.setIfTargetDownSendToQueue(false); 62 | config.setDownWordList(List.of("down")); 63 | 64 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 65 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 66 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 67 | KickEventHandler handler = new KickEventHandler(config, environment); 68 | 69 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("KickedPlayer"); 70 | QueueTestUtils.TestKickEvent event = QueueTestUtils.kickEvent(player, "target", "Server is down"); 71 | 72 | handler.handleKick(event); 73 | 74 | assertTrue(event.getCancelServer().isEmpty()); 75 | assertTrue(QueueTestUtils.defaultQueueType(config).getQueueMap().isEmpty()); 76 | } 77 | 78 | @Test 79 | void doesNotRedirectWhenKickedFromNonTargetServer() { 80 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 81 | config.setTargetServer("target"); 82 | config.setIfTargetDownSendToQueue(true); 83 | config.setDownWordList(List.of("down")); 84 | 85 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 86 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 87 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 88 | KickEventHandler handler = new KickEventHandler(config, environment); 89 | 90 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("KickedPlayer"); 91 | QueueTestUtils.TestKickEvent event = QueueTestUtils.kickEvent(player, "lobby", "Server is down"); 92 | 93 | handler.handleKick(event); 94 | 95 | assertTrue(event.getCancelServer().isEmpty()); 96 | assertTrue(QueueTestUtils.defaultQueueType(config).getQueueMap().isEmpty()); 97 | } 98 | 99 | @Test 100 | void doesNotRedirectWhenKickReasonDoesNotMatch() { 101 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 102 | config.setTargetServer("target"); 103 | config.setIfTargetDownSendToQueue(true); 104 | config.setDownWordList(List.of("down")); 105 | 106 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 107 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 108 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 109 | KickEventHandler handler = new KickEventHandler(config, environment); 110 | 111 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("KickedPlayer"); 112 | QueueTestUtils.TestKickEvent event = QueueTestUtils.kickEvent(player, "target", "You were kicked by an admin"); 113 | 114 | handler.handleKick(event); 115 | 116 | assertTrue(event.getCancelServer().isEmpty()); 117 | assertTrue(QueueTestUtils.defaultQueueType(config).getQueueMap().isEmpty()); 118 | } 119 | 120 | @Test 121 | void setsCustomKickMessageWhenEnabledAndWillDisconnect() { 122 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 123 | config.setEnableKickMessage(true); 124 | config.setKickMessage("Custom kick message"); 125 | 126 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 127 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 128 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 129 | KickEventHandler handler = new KickEventHandler(config, environment); 130 | 131 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("KickedPlayer"); 132 | QueueTestUtils.TestKickEvent event = QueueTestUtils.kickEvent(player, "target", "Some reason"); 133 | event.setWillDisconnect(true); 134 | 135 | handler.handleKick(event); 136 | 137 | assertEquals("Custom kick message", event.getKickMessage().orElseThrow()); 138 | } 139 | 140 | @Test 141 | void doesNotSetKickMessageWhenDisabled() { 142 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 143 | config.setEnableKickMessage(false); 144 | config.setKickMessage("Custom kick message"); 145 | 146 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 147 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 148 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 149 | KickEventHandler handler = new KickEventHandler(config, environment); 150 | 151 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("KickedPlayer"); 152 | QueueTestUtils.TestKickEvent event = QueueTestUtils.kickEvent(player, "target", "Some reason"); 153 | event.setWillDisconnect(true); 154 | 155 | handler.handleKick(event); 156 | 157 | assertTrue(event.getKickMessage().isEmpty()); 158 | } 159 | 160 | @Test 161 | void doesNotSetKickMessageWhenNotDisconnecting() { 162 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 163 | config.setEnableKickMessage(true); 164 | config.setKickMessage("Custom kick message"); 165 | 166 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 167 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 168 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 169 | KickEventHandler handler = new KickEventHandler(config, environment); 170 | 171 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("KickedPlayer"); 172 | QueueTestUtils.TestKickEvent event = QueueTestUtils.kickEvent(player, "target", "Some reason"); 173 | event.setWillDisconnect(false); 174 | 175 | handler.handleKick(event); 176 | 177 | assertTrue(event.getKickMessage().isEmpty()); 178 | } 179 | 180 | @Test 181 | void handlesKickWithoutReason() { 182 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 183 | config.setTargetServer("target"); 184 | config.setIfTargetDownSendToQueue(true); 185 | config.setDownWordList(List.of("down")); 186 | 187 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 188 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 189 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 190 | KickEventHandler handler = new KickEventHandler(config, environment); 191 | 192 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("KickedPlayer"); 193 | QueueTestUtils.TestKickEvent event = QueueTestUtils.kickEvent(player, "target", null); // No kick reason 194 | 195 | handler.handleKick(event); 196 | 197 | assertTrue(event.getCancelServer().isEmpty()); 198 | assertTrue(QueueTestUtils.defaultQueueType(config).getQueueMap().isEmpty()); 199 | } 200 | 201 | @Test 202 | void caseInsensitiveKickReasonMatching() { 203 | Config config = QueueTestUtils.createConfigWithSingleQueueType(5); 204 | config.setTargetServer("target"); 205 | config.setIfTargetDownSendToQueue(true); 206 | config.setDownWordList(List.of("server", "DOWN")); 207 | 208 | QueueTestUtils.TestQueuePlugin plugin = new QueueTestUtils.TestQueuePlugin(config); 209 | Set onlineServers = QueueTestUtils.onlineServers("queue", "target"); 210 | QueueEnvironment environment = new QueueEnvironment(plugin, plugin::getConfiguration, onlineServers); 211 | KickEventHandler handler = new KickEventHandler(config, environment); 212 | 213 | QueueTestUtils.TestPlayer player = plugin.registerPlayer("KickedPlayer"); 214 | QueueTestUtils.TestKickEvent event = QueueTestUtils.kickEvent(player, "target", "SERVER IS CURRENTLY down"); 215 | 216 | handler.handleKick(event); 217 | 218 | assertEquals("queue", event.getCancelServer().orElseThrow()); 219 | assertTrue(QueueTestUtils.defaultQueueType(config).getQueueMap().containsKey(player.getUniqueId())); 220 | } 221 | } 222 | --------------------------------------------------------------------------------