├── .dockerignore ├── .github └── workflows │ ├── docker-publish.yml │ └── trigger-jitpack-build.yml ├── .gitignore ├── Dockerfile ├── INTEGRATION.md ├── LICENSE ├── README.md ├── build-logic ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── server.common-conventions.gradle.kts ├── build.gradle.kts ├── common ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── bluedragonmc │ └── server │ ├── Constants.kt │ ├── CustomPlayer.kt │ ├── Game.kt │ ├── ModuleHolder.kt │ ├── VersionInfo.kt │ ├── api │ ├── DatabaseConnection.kt │ ├── Environment.kt │ ├── IncomingRPCHandler.kt │ ├── IncomingRPCHandlerStub.kt │ ├── OutgoingRPCHandler.kt │ ├── OutgoingRPCHandlerStub.kt │ ├── PermissionManager.kt │ └── Queue.kt │ ├── command │ ├── Arguments.kt │ └── BlueDragonCommand.kt │ ├── event │ ├── Cancellable.kt │ ├── CancellablePlayerEvent.kt │ ├── ChestOpenEvent.kt │ ├── ChestPopulateEvent.kt │ ├── CountdownEvent.kt │ ├── DataLoadedEvent.kt │ ├── GameEvent.kt │ ├── GameStartEvent.kt │ ├── GameStateChangedEvent.kt │ ├── KitSelectedEvent.kt │ ├── PlayerKillPlayerEvent.kt │ ├── PlayerLeaveGameEvent.kt │ ├── ProjectileBreakBlockEvent.kt │ └── TeamAssignedEvent.kt │ ├── model │ ├── DatabaseObjects.kt │ └── Serializers.kt │ ├── module │ ├── DependencyAnnotations.kt │ ├── GameModule.kt │ ├── GlobalCosmeticModule.kt │ ├── GuiModule.kt │ ├── MusicModule.kt │ ├── combat │ │ ├── CombatUtils.kt │ │ ├── CustomDeathMessageModule.kt │ │ ├── EnumArmorToughness.kt │ │ ├── OldCombatModule.kt │ │ └── ProjectileModule.kt │ ├── config │ │ ├── ConfigModule.kt │ │ └── serializer │ │ │ ├── BlockSerializer.kt │ │ │ ├── ColorSerializer.kt │ │ │ ├── ComponentSerializer.kt │ │ │ ├── EnchantmentListSerializer.kt │ │ │ ├── EntityTypeSerializer.kt │ │ │ ├── ItemStackSerializer.kt │ │ │ ├── KitSerializer.kt │ │ │ ├── MaterialSerializer.kt │ │ │ ├── PlayerSkinSerializer.kt │ │ │ └── PosSerializer.kt │ ├── database │ │ ├── AwardsModule.kt │ │ ├── CosmeticsModule.kt │ │ ├── StatRecorders.kt │ │ └── StatisticsModule.kt │ ├── gameplay │ │ ├── ActionBarModule.kt │ │ ├── ChestLootModule.kt │ │ ├── CombatZoneModule.kt │ │ ├── CustomItemModule.kt │ │ ├── DoubleJumpModule.kt │ │ ├── InstantRespawnModule.kt │ │ ├── InventoryPermissionsModule.kt │ │ ├── MapZonesModule.kt │ │ ├── MaxHealthModule.kt │ │ ├── NPCModule.kt │ │ ├── ShopModule.kt │ │ ├── SidebarModule.kt │ │ └── WorldPermissionsModule.kt │ ├── instance │ │ ├── CustomGeneratorInstanceModule.kt │ │ ├── InstanceContainerModule.kt │ │ ├── InstanceModule.kt │ │ ├── InstanceTimeModule.kt │ │ └── SharedInstanceModule.kt │ ├── map │ │ └── AnvilFileMapProviderModule.kt │ ├── minigame │ │ ├── CountdownModule.kt │ │ ├── KitsModule.kt │ │ ├── MOTDModule.kt │ │ ├── PlayerResetModule.kt │ │ ├── SpawnpointModule.kt │ │ ├── SpectatorModule.kt │ │ ├── TeamModule.kt │ │ ├── TimedRespawnModule.kt │ │ ├── VoidDeathModule.kt │ │ ├── VoteStartModule.kt │ │ └── WinModule.kt │ └── vanilla │ │ ├── ChestModule.kt │ │ ├── DoorsModule.kt │ │ ├── FallDamageModule.kt │ │ ├── FireworkRocketModule.kt │ │ ├── ItemDropModule.kt │ │ ├── ItemPickupModule.kt │ │ ├── NaturalRegenerationModule.kt │ │ └── PickItemModule.kt │ ├── service │ ├── Database.kt │ ├── Messaging.kt │ └── Permissions.kt │ └── utils │ ├── BlockUtils.kt │ ├── CircularList.kt │ ├── CoordinateUtils.kt │ ├── EventUtils.kt │ ├── FireworkUtils.kt │ ├── GameState.kt │ ├── InstanceUtils.kt │ ├── ItemUtils.kt │ ├── SoundUtils.kt │ ├── StringUtils.kt │ ├── TaskUtils.kt │ ├── TextUtils.kt │ └── packet │ └── GlowingEntityUtils.kt ├── docker-compose.yml ├── favicon_64.png ├── favicon_hq.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── production.Dockerfile ├── settings.gradle.kts └── src ├── main ├── kotlin-templates │ └── com │ │ └── bluedragonmc │ │ └── server │ │ └── GitVersionInfo.kt.peb ├── kotlin │ └── com │ │ └── bluedragonmc │ │ └── server │ │ ├── Server.kt │ │ ├── bootstrap │ │ ├── Bootstrap.kt │ │ ├── Commands.kt │ │ ├── CustomPlayerProvider.kt │ │ ├── DefaultDimensionTypes.kt │ │ ├── GlobalBlockHandlers.kt │ │ ├── GlobalChatFormat.kt │ │ ├── GlobalPlayerNameFormat.kt │ │ ├── GlobalPunishments.kt │ │ ├── GlobalTranslation.kt │ │ ├── IntegrationsInit.kt │ │ ├── PerInstanceChat.kt │ │ ├── PerInstanceTabList.kt │ │ ├── ServerListPingHandler.kt │ │ ├── TabListFormat.kt │ │ ├── dev │ │ │ ├── DevInstanceRouter.kt │ │ │ ├── MojangAuthentication.kt │ │ │ └── OpenToLAN.kt │ │ └── prod │ │ │ ├── AgonesIntegration.kt │ │ │ ├── InitialInstanceRouter.kt │ │ │ └── VelocityForwarding.kt │ │ ├── command │ │ ├── FlyCommand.kt │ │ ├── GameCommand.kt │ │ ├── GameModeCommand.kt │ │ ├── GiveCommand.kt │ │ ├── InstanceCommand.kt │ │ ├── JoinCommand.kt │ │ ├── KillCommand.kt │ │ ├── LeaderboardCommand.kt │ │ ├── ListCommand.kt │ │ ├── LobbyCommand.kt │ │ ├── MessageCommand.kt │ │ ├── MindecraftesCommand.kt │ │ ├── PartyCommand.kt │ │ ├── PingCommand.kt │ │ ├── PlaysoundCommand.kt │ │ ├── SetBlockCommand.kt │ │ ├── StopCommand.kt │ │ ├── TeleportCommand.kt │ │ ├── TimeCommand.kt │ │ ├── VersionCommand.kt │ │ └── punishment │ │ │ ├── KickCommand.kt │ │ │ ├── PardonCommand.kt │ │ │ ├── PunishCommand.kt │ │ │ ├── ViewPunishmentCommand.kt │ │ │ └── ViewPunishmentsCommand.kt │ │ ├── impl │ │ ├── DatabaseConnectionImpl.kt │ │ ├── IncomingRPCHandlerImpl.kt │ │ ├── OutgoingRPCHandlerImpl.kt │ │ └── PermissionManagerImpl.kt │ │ └── queue │ │ ├── GameClassLoader.kt │ │ ├── GameLoader.kt │ │ ├── IPCQueue.kt │ │ ├── TestQueue.kt │ │ └── environments.kt └── resources │ ├── bd-banner.png │ ├── config │ └── cosmetics.yml │ ├── i18n.properties │ ├── lang_en.properties │ ├── lang_en_pt.properties │ ├── lang_zh_cn.properties │ └── tinylog.properties └── test └── kotlin └── com └── bluedragonmc └── server ├── GlobalTranslatorTest.kt ├── game └── ModuleHolderTest.kt └── utils └── TextUtilsTest.kt /.dockerignore: -------------------------------------------------------------------------------- 1 | # Gradle temporary files 2 | .gradle 3 | # IntelliJ project files 4 | .idea 5 | # Assets 6 | worlds 7 | favicon_hq.png 8 | README.md 9 | # Built classes 10 | build -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [ "main" ] 7 | push: 8 | branches: [ "main" ] 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | # github.repository as / 13 | IMAGE_NAME: ${{ github.repository }} 14 | 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 20 21 | permissions: 22 | contents: read 23 | packages: write 24 | # This is used to complete the identity challenge 25 | # with sigstore/fulcio when running outside of PRs. 26 | id-token: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v3 31 | 32 | # Workaround: https://github.com/docker/build-push-action/issues/461 33 | - name: Setup Docker buildx 34 | uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf 35 | 36 | # Login against a Docker registry except on PR 37 | # https://github.com/docker/login-action 38 | - name: Log into registry ${{ env.REGISTRY }} 39 | if: github.event_name != 'pull_request' 40 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 41 | with: 42 | registry: ${{ env.REGISTRY }} 43 | username: ${{ github.actor }} 44 | password: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | # Extract metadata (tags, labels) for Docker 47 | # https://github.com/docker/metadata-action 48 | - name: Extract Docker metadata 49 | id: meta 50 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 51 | with: 52 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 53 | tags: | 54 | type=raw,value=latest 55 | type=sha,format=long 56 | type=ref,event=branch 57 | 58 | # Build and push Docker image with Buildx (don't push on PR) 59 | # https://github.com/docker/build-push-action 60 | - name: Build and push Docker image 61 | id: build-and-push 62 | uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a 63 | with: 64 | context: . 65 | push: ${{ github.event_name != 'pull_request' }} 66 | tags: ${{ steps.meta.outputs.tags }} 67 | labels: ${{ steps.meta.outputs.labels }} 68 | cache-from: type=gha 69 | cache-to: type=gha,mode=max 70 | -------------------------------------------------------------------------------- /.github/workflows/trigger-jitpack-build.yml: -------------------------------------------------------------------------------- 1 | # Taken from Minestom under the Apache License 2.0 2 | # https://github.com/Minestom/Minestom/blob/master/.github/workflows/trigger-jitpack-build.yml 3 | # https://github.com/Minestom/Minestom/blob/master/LICENSE 4 | name: Trigger Jitpack Build 5 | on: 6 | push: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 20 13 | steps: 14 | - name: Trigger Jitpack Build 15 | run: curl "https://jitpack.io/com/github/BlueDragonMC/Server/${GITHUB_SHA:0:10}/build.log" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | **/build/ 3 | !src/**/build/ 4 | games/ 5 | 6 | # IntelliJ project folder 7 | .idea 8 | build 9 | 10 | # Ignore Gradle GUI config 11 | gradle-app.setting 12 | 13 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 14 | !gradle-wrapper.jar 15 | 16 | # Avoid ignore Gradle wrappper properties 17 | !gradle-wrapper.properties 18 | 19 | # Cache of project 20 | .gradletasknamecache 21 | 22 | # Eclipse Gradle plugin generated files 23 | # Eclipse Core 24 | .project 25 | # JDT-specific (Eclipse Java Development Tools) 26 | .classpath 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.2 2 | # This Dockerfile must be run with BuildKit enabled 3 | # see https://docs.docker.com/engine/reference/builder/#buildkit 4 | # for the Dockerfile that is run on our CI/CD pipeline, see production.Dockerfile 5 | 6 | # Build the project into an executable JAR 7 | FROM gradle:jdk21 as build 8 | # Copy build files and source code 9 | COPY . /work 10 | WORKDIR /work 11 | # Run gradle in the /work directory 12 | RUN --mount=target=/home/gradle/.gradle,type=cache \ 13 | /usr/bin/gradle --console=plain --info --stacktrace --no-daemon -x test --build-cache build 14 | 15 | # Run the built JAR and expose port 25565 16 | FROM eclipse-temurin:21-jre-alpine 17 | EXPOSE 25565 18 | EXPOSE 50051 19 | WORKDIR /server 20 | 21 | LABEL com.bluedragonmc.image=server 22 | LABEL com.bluedragonmc.environment=development 23 | 24 | # Copy config files and assets 25 | COPY favicon_64.png /server/favicon_64.png 26 | 27 | # Copy the built JAR from the previous step 28 | COPY --from=build /work/build/libs/Server-*-all.jar /server/server.jar 29 | 30 | # Run the server 31 | CMD ["java", "-jar", "/server/server.jar"] -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | gradlePluginPortal() 7 | } 8 | 9 | val libs = extensions.getByType().named("libs") 10 | 11 | dependencies { 12 | implementation(libs.findLibrary("kotlin-jvm").get()) 13 | } 14 | -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | dependencyResolutionManagement { 3 | versionCatalogs { 4 | create("libs") { 5 | from(files("../gradle/libs.versions.toml")) 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/server.common-conventions.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | plugins { 5 | id("org.jetbrains.kotlin.jvm") 6 | } 7 | 8 | tasks.withType { 9 | compilerOptions { 10 | jvmTarget.set(JvmTarget.JVM_21) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("server.common-conventions") 3 | id("com.github.johnrengelman.shadow") version "8.1.1" 4 | kotlin("plugin.serialization") version "2.1.10" 5 | id("net.kyori.blossom") version "2.1.0" 6 | `maven-publish` 7 | } 8 | 9 | group = "com.bluedragonmc" 10 | version = "1.0-SNAPSHOT" 11 | 12 | repositories { 13 | mavenLocal() 14 | mavenCentral() 15 | maven(url = "https://jitpack.io") 16 | maven("https://reposilite.atlasengine.ca/public") 17 | } 18 | 19 | dependencies { 20 | testImplementation(kotlin("test")) 21 | testImplementation(libs.mockk) 22 | testImplementation(libs.junit.api) 23 | testRuntimeOnly(libs.junit.engine) 24 | 25 | implementation(libs.minestom) // Minestom 26 | implementation(libs.minimessage) // MiniMessage 27 | implementation(libs.kmongo) // Database support 28 | implementation(libs.caffeine) // Caching library for database responses 29 | implementation(libs.bundles.configurate) // Configurate for game configuration 30 | implementation(libs.bundles.messaging) // Messaging 31 | implementation(libs.okhttp) 32 | 33 | implementation(project(":common")) 34 | } 35 | 36 | sourceSets { 37 | main { 38 | blossom { 39 | val gitCommit = getOutputOf("git rev-parse --verify --short HEAD") 40 | val gitBranch = getOutputOf("git rev-parse --abbrev-ref HEAD") 41 | val gitCommitDate = getOutputOf("git log -1 --format=%ct") 42 | 43 | kotlinSources { 44 | property("gitCommit", gitCommit) 45 | property("gitBranch", gitBranch) 46 | property("gitCommitDate", gitCommitDate) 47 | } 48 | } 49 | } 50 | } 51 | 52 | fun getOutputOf(command: String): String? { 53 | try { 54 | val stream = org.apache.commons.io.output.ByteArrayOutputStream() 55 | project.exec { 56 | commandLine = command.split(" ") 57 | standardOutput = stream 58 | } 59 | return String(stream.toByteArray()).trim() 60 | } catch (e: Throwable) { 61 | return null 62 | } 63 | } 64 | 65 | publishing { 66 | publications { 67 | create("maven") { 68 | groupId = "com.bluedragonmc" 69 | artifactId = "Server" 70 | version = "1.0" 71 | 72 | from(components["java"]) 73 | } 74 | } 75 | } 76 | 77 | kotlin { 78 | jvmToolchain { 79 | languageVersion.set(JavaLanguageVersion.of(21)) 80 | } 81 | } 82 | 83 | tasks.test { 84 | useJUnitPlatform() 85 | } 86 | 87 | tasks.shadowJar { 88 | mergeServiceFiles() 89 | } 90 | 91 | tasks.jar { 92 | dependsOn(tasks.shadowJar) 93 | manifest { 94 | attributes["Main-Class"] = "com.bluedragonmc.server.ServerKt" 95 | } 96 | } -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("server.common-conventions") 3 | kotlin("plugin.serialization") version "2.1.10" 4 | `maven-publish` 5 | } 6 | 7 | group = "com.bluedragonmc.server" 8 | version = "1.0-SNAPSHOT" 9 | 10 | repositories { 11 | mavenLocal() 12 | mavenCentral() 13 | maven(url = "https://jitpack.io") 14 | maven("https://reposilite.atlasengine.ca/public") 15 | } 16 | 17 | dependencies { 18 | implementation(libs.minestom) 19 | implementation(libs.atlas.projectiles) 20 | implementation(libs.kmongo) 21 | implementation(libs.caffeine) 22 | implementation(libs.minimessage) 23 | implementation(libs.bundles.configurate) 24 | implementation(libs.bundles.messaging) 25 | implementation(libs.serialization.json) 26 | implementation(libs.bundles.tinylog) 27 | } 28 | 29 | publishing { 30 | publications { 31 | create("maven") { 32 | groupId = "com.bluedragonmc.server" 33 | artifactId = "common" 34 | version = "1.0" 35 | 36 | from(components["java"]) 37 | } 38 | } 39 | } 40 | 41 | tasks.getByName("test") { 42 | useJUnitPlatform() 43 | } -------------------------------------------------------------------------------- /common/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | project.name="common" -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server 2 | 3 | import com.bluedragonmc.server.utils.withGradient 4 | import net.kyori.adventure.text.Component 5 | import net.kyori.adventure.text.format.NamedTextColor 6 | import net.kyori.adventure.text.format.TextColor 7 | import java.io.File 8 | import java.nio.charset.Charset 9 | import java.util.* 10 | 11 | /** 12 | * The namespace used for custom 13 | * BlueDragon registry keys. 14 | */ 15 | const val NAMESPACE = "bluedragon" 16 | 17 | /** 18 | * The default locale used when flattening 19 | * and translating components. 20 | */ 21 | val DEFAULT_LOCALE: Locale = Locale.ENGLISH 22 | 23 | /** 24 | * Light color, often used for emphasis. 25 | */ 26 | val BRAND_COLOR_PRIMARY_1 = TextColor.color(0x4EB2F4) 27 | 28 | /** 29 | * Medium color, often used for chat messages. 30 | */ 31 | val BRAND_COLOR_PRIMARY_2 = TextColor.color(0x2792f7) // Medium, often used for chat messages 32 | 33 | /** 34 | * Very dark color. 35 | */ 36 | val BRAND_COLOR_PRIMARY_3 = TextColor.color(0x3336f4) // Very dark 37 | 38 | /** 39 | * An alternate color, used for the title in the scoreboard and some chat messages. 40 | */ 41 | val ALT_COLOR_1: TextColor = NamedTextColor.YELLOW 42 | 43 | /** 44 | * A secondary alternate color that complements [ALT_COLOR_1]. 45 | */ 46 | val ALT_COLOR_2: TextColor = NamedTextColor.GOLD 47 | 48 | /** 49 | * The name of the server ("BlueDragon") with a nice gradient. This is not bold. 50 | */ 51 | val SERVER_NAME_GRADIENT = Component.text("BlueDragon").withGradient(BRAND_COLOR_PRIMARY_1, BRAND_COLOR_PRIMARY_3) 52 | 53 | /** 54 | * A base64-encoded PNG image of the server's favicon shown on clients' server lists. 55 | */ 56 | val FAVICON = "data:image/png;base64," + runCatching { 57 | String(Base64.getEncoder().encode(File("favicon_64.png").readBytes()), Charset.forName("UTF-8")) 58 | }.getOrElse { "" } 59 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/VersionInfo.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server 2 | 3 | import java.util.* 4 | 5 | interface VersionInfo { 6 | 7 | val COMMIT: String? 8 | val BRANCH: String? 9 | val COMMIT_DATE: String? 10 | 11 | val commitDate: Date? 12 | get() = COMMIT_DATE?.toLongOrNull()?.let { 13 | Date(it * 1_000) 14 | } 15 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/api/DatabaseConnection.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.api 2 | 3 | import com.bluedragonmc.server.CustomPlayer 4 | import com.bluedragonmc.server.model.GameDocument 5 | import com.bluedragonmc.server.model.PlayerDocument 6 | import net.minestom.server.entity.Player 7 | import java.util.* 8 | import kotlin.reflect.KMutableProperty 9 | 10 | interface DatabaseConnection { 11 | 12 | suspend fun loadDataDocument(player: CustomPlayer) 13 | 14 | suspend fun getPlayerDocument(username: String): PlayerDocument? 15 | 16 | suspend fun getPlayerDocument(uuid: UUID): PlayerDocument? 17 | 18 | suspend fun getPlayerDocument(player: Player): PlayerDocument 19 | 20 | suspend fun rankPlayersByStatistic(key: String, sortCriteria: String, limit: Int): List 21 | 22 | suspend fun getPlayerForPunishmentId(id: String): PlayerDocument? 23 | 24 | suspend fun updatePlayer(playerUuid: String, field: KMutableProperty, value: T) 25 | 26 | suspend fun logGame(game: GameDocument) 27 | } 28 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/api/Environment.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.api 2 | 3 | import com.bluedragonmc.server.VersionInfo 4 | 5 | abstract class Environment { 6 | 7 | companion object { 8 | 9 | lateinit var current: Environment 10 | 11 | val queue get() = current.queue 12 | val mongoConnectionString get() = current.mongoConnectionString 13 | val dbName get() = current.dbName 14 | val puffinHostname get() = current.puffinHostname 15 | val puffinPort get() = current.puffinPort 16 | val grpcServerPort get() = current.grpcServerPort 17 | val defaultGameName get() = current.defaultGameName 18 | val gameClasses get() = current.gameClasses 19 | val versionInfo get() = current.versionInfo 20 | val isDev get() = current.isDev 21 | suspend fun getServerName() = current.getServerName() 22 | 23 | fun setEnvironment(env: Environment) { 24 | if (!::current.isInitialized) { 25 | current = env 26 | } else error("Tried to override current Environment") 27 | } 28 | } 29 | 30 | abstract val queue: Queue 31 | abstract val mongoConnectionString: String 32 | abstract val puffinHostname: String 33 | abstract val puffinPort: Int 34 | abstract val grpcServerPort: Int 35 | abstract val luckPermsHostname: String 36 | abstract val gameClasses: Collection 37 | abstract val versionInfo: VersionInfo 38 | abstract val isDev: Boolean 39 | open val defaultGameName: String = "Lobby" 40 | open val dbName: String = "bluedragon" 41 | 42 | abstract suspend fun getServerName(): String 43 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/api/IncomingRPCHandler.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.api 2 | 3 | /** 4 | * Represents a messaging handler that receives messages 5 | * from other services and performs actions based on them. 6 | */ 7 | interface IncomingRPCHandler { 8 | 9 | fun isConnected(): Boolean 10 | 11 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/api/IncomingRPCHandlerStub.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.api 2 | 3 | /** 4 | * Stub - no functionality. Used in development and testing environments. 5 | * See [com.bluedragonmc.server.impl.IncomingRPCHandlerImpl] 6 | * for a full implementation. 7 | */ 8 | class IncomingRPCHandlerStub : IncomingRPCHandler { 9 | override fun isConnected(): Boolean { 10 | return true 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/api/OutgoingRPCHandler.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.api 2 | 3 | import com.bluedragonmc.api.grpc.CommonTypes.GameState 4 | import com.bluedragonmc.api.grpc.CommonTypes.GameType 5 | import com.bluedragonmc.api.grpc.JukeboxOuterClass.PlayerSongQueue 6 | import com.bluedragonmc.api.grpc.PartySvc.PartyListResponse 7 | import com.bluedragonmc.api.grpc.PlayerTrackerOuterClass.QueryPlayerResponse 8 | import com.bluedragonmc.server.Game 9 | import net.kyori.adventure.text.Component 10 | import net.minestom.server.command.CommandSender 11 | import net.minestom.server.entity.Player 12 | import java.util.* 13 | 14 | /** 15 | * Represents a messaging handler that sends messages 16 | * from this service to other services. 17 | */ 18 | interface OutgoingRPCHandler { 19 | 20 | fun isConnected(): Boolean 21 | 22 | suspend fun initGameServer(serverName: String) 23 | 24 | // Instance tracking 25 | fun onGameCreated(game: Game) 26 | suspend fun initGame(id: String, gameType: GameType, gameState: GameState) 27 | suspend fun updateGameState(id: String, gameState: GameState) 28 | suspend fun notifyInstanceRemoved(gameId: String) 29 | suspend fun checkRemoveInstance(gameId: String): Boolean 30 | 31 | // Player tracking 32 | suspend fun recordInstanceChange(player: Player, newGame: String) 33 | suspend fun playerTransfer(player: Player, newGame: String?) 34 | suspend fun queryPlayer(username: String? = null, uuid: UUID? = null): QueryPlayerResponse 35 | 36 | // Queue 37 | suspend fun addToQueue(player: Player, gameType: GameType) 38 | suspend fun removeFromQueue(player: Player) 39 | suspend fun getDestination(player: UUID): String? 40 | 41 | // Private messaging 42 | suspend fun sendPrivateMessage(message: Component, sender: CommandSender, recipient: UUID) 43 | 44 | // Party system 45 | suspend fun inviteToParty(partyOwner: UUID, invitee: UUID) 46 | suspend fun acceptPartyInvitation(partyOwner: UUID, invitee: UUID) 47 | suspend fun kickFromParty(partyOwner: UUID, player: UUID) 48 | suspend fun leaveParty(player: UUID) 49 | suspend fun partyChat(message: String, sender: Player) 50 | suspend fun warpParty(partyOwner: Player, gameId: String) 51 | suspend fun transferParty(partyOwner: Player, newOwner: UUID) 52 | suspend fun listPartyMembers(member: UUID): PartyListResponse 53 | 54 | // Party Marathons 55 | suspend fun startMarathon(player: UUID, durationMs: Int) 56 | suspend fun endMarathon(player: UUID) 57 | suspend fun getMarathonLeaderboard(players: Collection, silent: Boolean) 58 | suspend fun recordCoinAward(player: UUID, coins: Int, gameId: String) 59 | 60 | // Jukebox controls 61 | suspend fun getSongInfo(player: Player): PlayerSongQueue 62 | suspend fun playSong(player: Player, songName: String, queuePosition: Int, startTimeInTicks: Int, tags: List): Boolean 63 | suspend fun removeSongByName(player: Player, songName: String) 64 | suspend fun removeSongByTag(player: Player, matchTags: List) 65 | suspend fun stopSongAndClearQueue(player: Player) 66 | 67 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/api/PermissionManager.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.api 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.kyori.adventure.text.format.TextColor 5 | import java.util.* 6 | 7 | interface PermissionManager { 8 | fun getMetadata(player: UUID): PlayerMeta 9 | fun hasPermission(player: UUID, node: String): Boolean? 10 | } 11 | 12 | data class PlayerMeta(val prefix: Component, val suffix: Component, val primaryGroup: String, val rankColor: TextColor) 13 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/api/Queue.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.api 2 | 3 | import com.bluedragonmc.api.grpc.CommonTypes 4 | import com.bluedragonmc.api.grpc.GsClient 5 | import com.bluedragonmc.api.grpc.PlayerHolderOuterClass.SendPlayerRequest 6 | import com.bluedragonmc.server.Game 7 | import net.minestom.server.entity.Player 8 | import java.io.File 9 | 10 | abstract class Queue { 11 | 12 | abstract fun start() 13 | abstract fun queue(player: Player, gameType: CommonTypes.GameType) 14 | 15 | abstract fun getMaps(gameType: String): Array? 16 | abstract fun randomMap(gameType: String): String? 17 | 18 | open fun createInstance(request: GsClient.CreateInstanceRequest): Game? { 19 | throw NotImplementedError("Creating instances not implemented") 20 | } 21 | 22 | open fun sendPlayer(request: SendPlayerRequest) {} 23 | 24 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/Cancellable.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import net.minestom.server.event.trait.CancellableEvent 4 | 5 | abstract class Cancellable : CancellableEvent { 6 | private var cancelled = false 7 | 8 | override fun isCancelled(): Boolean = cancelled 9 | 10 | override fun setCancelled(cancel: Boolean) { 11 | cancelled = cancel 12 | } 13 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/CancellablePlayerEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import net.minestom.server.entity.Player 4 | import net.minestom.server.event.trait.PlayerEvent 5 | 6 | /** 7 | * An event which is cancellable and player-specific. 8 | */ 9 | abstract class CancellablePlayerEvent(private val player: Player) : Cancellable(), PlayerEvent { 10 | private var cancelled = false 11 | 12 | override fun isCancelled(): Boolean = cancelled 13 | 14 | override fun setCancelled(cancel: Boolean) { 15 | cancelled = cancel 16 | } 17 | 18 | override fun getPlayer() = player 19 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/ChestOpenEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.module.GuiModule 4 | import net.minestom.server.coordinate.Point 5 | import net.minestom.server.entity.Player 6 | import net.minestom.server.event.trait.PlayerInstanceEvent 7 | import net.minestom.server.inventory.InventoryType 8 | 9 | /** 10 | * Called whenever a chest is opened. 11 | * If this event is cancelled, the chest's [menu] is not opened for the [player]. 12 | */ 13 | data class ChestOpenEvent( 14 | private val player: Player, 15 | val position: Point, 16 | val baseChestPosition: Point, 17 | val inventoryType: InventoryType, 18 | val menu: GuiModule.Menu, 19 | ) : PlayerInstanceEvent, Cancellable() { 20 | override fun getPlayer(): Player = player 21 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/ChestPopulateEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.module.GuiModule 4 | import net.minestom.server.coordinate.Point 5 | import net.minestom.server.entity.Player 6 | import net.minestom.server.event.trait.PlayerInstanceEvent 7 | import net.minestom.server.inventory.InventoryType 8 | 9 | /** 10 | * Called when a chest is first populated. 11 | * This event is not per-player, it is only called when a chest's [menu] is created. 12 | * The player in this event is always the first [player] to open the chest at [baseChestPosition]. 13 | */ 14 | data class ChestPopulateEvent( 15 | private val player: Player, 16 | val position: Point, 17 | val baseChestPosition: Point, 18 | val inventoryType: InventoryType, 19 | val menu: GuiModule.Menu, 20 | ) : PlayerInstanceEvent { 21 | override fun getPlayer(): Player = player 22 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/CountdownEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.Game 4 | 5 | class CountdownEvent { 6 | class CountdownStartEvent(game: Game) : GameEvent(game) 7 | class CountdownTickEvent(game: Game, val secondsLeft: Int) : GameEvent(game) 8 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/DataLoadedEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import net.minestom.server.entity.Player 4 | import net.minestom.server.event.trait.PlayerEvent 5 | 6 | /** 7 | * Called when a player's data document has been fetched from the database. 8 | * Will always be called after AsyncPlayerConfigurationEvent, and the [player] 9 | * will always be an instance of `CustomPlayer` with an initialized `data` field. 10 | */ 11 | class DataLoadedEvent(private val player: Player) : PlayerEvent { 12 | override fun getPlayer(): Player = player 13 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/GameEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.Game 4 | 5 | abstract class GameEvent(val game: Game) : Cancellable() -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/GameStartEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.Game 4 | 5 | /** 6 | * Called by the CountdownModule when the game starts. 7 | * This event cannot be canceled. 8 | */ 9 | class GameStartEvent(game: Game) : GameEvent(game) -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/GameStateChangedEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.utils.GameState 5 | 6 | /** 7 | * Called when a game's state is changed. 8 | * Used to propagate state updates to external services, 9 | * such as with the MessagingModule. 10 | */ 11 | class GameStateChangedEvent(game: Game, val oldState: GameState, val newState: GameState) : GameEvent(game) 12 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/KitSelectedEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.minigame.KitsModule 5 | import net.minestom.server.entity.Player 6 | 7 | /** 8 | * This event is fired when the player confirms their kit selection. 9 | * If the player closes the kit selection menu, this event is not fired. 10 | */ 11 | class KitSelectedEvent(game: Game, val player: Player, val kit: KitsModule.Kit) : GameEvent(game) -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/PlayerKillPlayerEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import net.minestom.server.entity.Player 4 | 5 | /** 6 | * Can be called by a module when a player has killed another player. 7 | */ 8 | class PlayerKillPlayerEvent(val attacker: Player, val target: Player) : CancellablePlayerEvent(attacker) -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/PlayerLeaveGameEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.Game 4 | import net.minestom.server.entity.Player 5 | import net.minestom.server.event.trait.PlayerEvent 6 | 7 | /** 8 | * Called when a player leaves the game, either by joining a different game or disconnecting from the server. 9 | */ 10 | class PlayerLeaveGameEvent(game: Game, private val player: Player) : GameEvent(game), PlayerEvent { 11 | override fun getPlayer() = player 12 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/ProjectileBreakBlockEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.Game 4 | import net.minestom.server.coordinate.Point 5 | import net.minestom.server.entity.Player 6 | import net.minestom.server.event.trait.PlayerEvent 7 | import net.minestom.server.instance.block.Block 8 | 9 | class ProjectileBreakBlockEvent( 10 | game: Game, 11 | private val shooter: Player, 12 | val block: Block, 13 | val blockPosition: Point 14 | ) : GameEvent(game), PlayerEvent { 15 | override fun getPlayer() = shooter 16 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/event/TeamAssignedEvent.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.event 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.minigame.TeamModule 5 | import net.minestom.server.entity.Player 6 | import net.minestom.server.event.trait.PlayerEvent 7 | 8 | class TeamAssignedEvent(game: Game, private val player: Player, val team: TeamModule.Team) : GameEvent(game), PlayerEvent { 9 | override fun getPlayer(): Player = player 10 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/model/Serializers.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package com.bluedragonmc.server.model 4 | 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.descriptors.PrimitiveKind 8 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 9 | import kotlinx.serialization.descriptors.SerialDescriptor 10 | import kotlinx.serialization.encoding.Decoder 11 | import kotlinx.serialization.encoding.Encoder 12 | import java.util.* 13 | 14 | open class ToStringSerializer( 15 | descriptorName: String, 16 | private val toStringMethod: (T) -> String, 17 | private val fromStringMethod: (String) -> T, 18 | ) : KSerializer { 19 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(descriptorName, PrimitiveKind.STRING) 20 | 21 | override fun deserialize(decoder: Decoder): T = fromStringMethod(decoder.decodeString()) 22 | 23 | override fun serialize(encoder: Encoder, value: T) { 24 | encoder.encodeString(toStringMethod(value)) 25 | } 26 | } 27 | 28 | object UUIDSerializer : ToStringSerializer("UUID", UUID::toString, UUID::fromString) 29 | 30 | object DateSerializer : KSerializer { 31 | override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG) 32 | override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) 33 | override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) 34 | } 35 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/DependencyAnnotations.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * An annotation used to define module dependencies. 7 | * All modules specified in the [dependencies] parameter 8 | * will be initialized before the module which is being 9 | * annotated, and an error will be thrown if the 10 | * dependencies were not found. 11 | */ 12 | @Target(AnnotationTarget.CLASS) 13 | annotation class DependsOn(vararg val dependencies: KClass) 14 | 15 | /** 16 | * An annotation used to define "soft dependencies". 17 | * All modules specified in the [dependencies] parameter 18 | * will be initialized before the module which is being 19 | * annotated. 20 | * 21 | * Unlike [DependsOn], an error will not be thrown if 22 | * a soft dependency is not found. 23 | */ 24 | @Target(AnnotationTarget.CLASS) 25 | annotation class SoftDependsOn(vararg val dependencies: KClass) 26 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/GameModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module 2 | 3 | import com.bluedragonmc.server.Game 4 | import net.minestom.server.event.Event 5 | import net.minestom.server.event.EventNode 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory 8 | import kotlin.reflect.full.findAnnotation 9 | 10 | abstract class GameModule { 11 | 12 | open val eventPriority = 0 13 | 14 | lateinit var eventNode: EventNode 15 | 16 | abstract fun initialize(parent: Game, eventNode: EventNode) 17 | open fun deinitialize() {} 18 | 19 | val logger: Logger by lazy { 20 | LoggerFactory.getLogger(javaClass) 21 | } 22 | 23 | open fun getRequiredDependencies() = this::class.findAnnotation()?.dependencies ?: emptyArray() 24 | open fun getSoftDependencies() = this::class.findAnnotation()?.dependencies ?: emptyArray() 25 | 26 | fun getDependencies() = arrayOf(*getRequiredDependencies(), *getSoftDependencies()) 27 | 28 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/GlobalCosmeticModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.database.CosmeticsModule 5 | import net.kyori.adventure.text.format.NamedTextColor 6 | import net.kyori.adventure.text.format.TextColor 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.event.Event 9 | import net.minestom.server.event.EventNode 10 | 11 | @DependsOn(CosmeticsModule::class) 12 | class GlobalCosmeticModule : GameModule() { 13 | 14 | private lateinit var cosmetics: CosmeticsModule 15 | 16 | override fun initialize(parent: Game, eventNode: EventNode) { 17 | cosmetics = parent.getModule() 18 | } 19 | 20 | fun getFireworkColor(player: Player) = cosmetics.getCosmeticInGroup(player)?.fireworkColors ?: emptyArray() 21 | 22 | @Suppress("unused") 23 | enum class WinFireworks(override val id: String, vararg val fireworkColors: TextColor) : CosmeticsModule.Cosmetic { 24 | RED("global_firework_red", NamedTextColor.RED), 25 | ORANGE("global_firework_orange", TextColor.color(239, 147, 43)), 26 | YELLOW("global_firework_yellow", NamedTextColor.YELLOW), 27 | GREEN("global_firework_green", NamedTextColor.GREEN), 28 | BLUE("global_firework_blue", TextColor.color(79, 185, 242)), 29 | PURPLE("global_firework_purple", TextColor.color(131, 71, 229)), 30 | RAINBOW("global_firework_rainbow", *NamedTextColor.NAMES.values().toTypedArray()), 31 | } 32 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/MusicModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.service.Messaging 5 | import kotlinx.coroutines.launch 6 | import net.minestom.server.entity.Player 7 | import net.minestom.server.event.Event 8 | import net.minestom.server.event.EventNode 9 | 10 | class MusicModule : GameModule() { 11 | override fun initialize(parent: Game, eventNode: EventNode) { 12 | 13 | } 14 | 15 | fun play(player: Player, songName: String, queuePosition: Int, startTimeInTicks: Int, tags: List) { 16 | Messaging.IO.launch { 17 | Messaging.outgoing.playSong(player, songName, queuePosition, startTimeInTicks, tags) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/combat/CombatUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.combat 2 | 3 | import net.minestom.server.component.DataComponents 4 | import net.minestom.server.entity.Entity 5 | import net.minestom.server.entity.EntityType 6 | import net.minestom.server.item.ItemStack 7 | import net.minestom.server.item.enchant.Enchantment 8 | import net.minestom.server.registry.RegistryKey 9 | import kotlin.random.Random 10 | 11 | object CombatUtils { 12 | fun damageItemStack(itemStack: ItemStack, amount: Int): ItemStack { 13 | val unbreakingLevel = itemStack.get(DataComponents.ENCHANTMENTS)?.enchantments?.get(Enchantment.UNBREAKING) ?: 0 14 | val processedAmount = (0 until amount).count { !shouldRestoreDurability(itemStack, unbreakingLevel) } 15 | // todo - if the damage increases beyond the max damage (durability), do we need to manually delete the item? 16 | return itemStack.with(DataComponents.DAMAGE, (itemStack.get(DataComponents.DAMAGE) ?: 0) + processedAmount) 17 | } 18 | 19 | private fun shouldRestoreDurability(itemStack: ItemStack, unbreakingLevel: Int): Boolean { 20 | // see https://minecraft.fandom.com/wiki/Unbreaking?so=search#Usage 21 | if (itemStack.material().isArmor) { 22 | if (Math.random() >= (0.6 + 0.4 / (unbreakingLevel + 1))) return false 23 | } else { 24 | if (Math.random() >= 1.0 / (unbreakingLevel + 1)) return false 25 | } 26 | return true 27 | } 28 | 29 | fun shouldCauseThorns(level: Int): Boolean = if (level <= 0) false else Random.nextFloat() < 0.15 * level 30 | 31 | fun getThornsDamage(level: Int): Int = if (level > 10) 10 - level else 1 + Random.nextInt(4) 32 | 33 | fun getDamageModifier(enchants: Map, Int>, targetEntity: Entity): Float = 34 | if (enchants.containsKey(Enchantment.SHARPNESS)) { 35 | enchants[Enchantment.SHARPNESS]!! * 1.25f 36 | } else if (enchants.containsKey(Enchantment.SMITE) && isUndead(targetEntity)) { 37 | enchants[Enchantment.SMITE]!! * 2.5f 38 | } else if (enchants.containsKey(Enchantment.BANE_OF_ARTHROPODS) && isArthropod(targetEntity)) { 39 | enchants[Enchantment.BANE_OF_ARTHROPODS]!! * 2.5f 40 | } else 0.0f 41 | 42 | fun getArrowDamageModifier(enchants: Map, Int>, targetEntity: Entity): Float = 43 | if (enchants.containsKey(Enchantment.POWER)) 44 | 0.5f + enchants[Enchantment.POWER]!! * 0.5f 45 | else 0.0f 46 | 47 | 48 | private val UNDEAD_MOBS = setOf( 49 | EntityType.DROWNED, 50 | EntityType.HUSK, 51 | EntityType.PHANTOM, 52 | EntityType.SKELETON, 53 | EntityType.SKELETON_HORSE, 54 | EntityType.STRAY, 55 | EntityType.WITHER, 56 | EntityType.WITHER_SKELETON, 57 | EntityType.ZOGLIN, 58 | EntityType.ZOMBIE, 59 | EntityType.ZOMBIE_VILLAGER, 60 | EntityType.ZOMBIFIED_PIGLIN, 61 | ) 62 | 63 | private fun isUndead(entity: Entity) = UNDEAD_MOBS.contains(entity.entityType) 64 | 65 | private fun isArthropod(entity: Entity) = 66 | entity.entityType == EntityType.SPIDER || entity.entityType == EntityType.CAVE_SPIDER || entity.entityType == EntityType.ENDERMITE || entity.entityType == EntityType.SILVERFISH 67 | 68 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/combat/CustomDeathMessageModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.combat 2 | 3 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_2 4 | import com.bluedragonmc.server.Game 5 | import com.bluedragonmc.server.module.GameModule 6 | import com.bluedragonmc.server.utils.GameState 7 | import net.kyori.adventure.text.Component 8 | import net.minestom.server.entity.Player 9 | import net.minestom.server.entity.damage.DamageType 10 | import net.minestom.server.entity.damage.EntityDamage 11 | import net.minestom.server.entity.damage.EntityProjectileDamage 12 | import net.minestom.server.event.Event 13 | import net.minestom.server.event.EventNode 14 | import net.minestom.server.event.player.PlayerDeathEvent 15 | 16 | class CustomDeathMessageModule : GameModule() { 17 | 18 | override fun initialize(parent: Game, eventNode: EventNode) { 19 | eventNode.addListener(PlayerDeathEvent::class.java) { event -> 20 | if (parent.state != GameState.INGAME) { 21 | event.chatMessage = null 22 | return@addListener 23 | } 24 | val player = event.player 25 | event.chatMessage = when (val src = event.player.lastDamageSource) { 26 | is EntityDamage -> { 27 | val playerName = (src.source as? Player)?.name 28 | if (playerName != null) { 29 | Component.translatable("death.attack.player", BRAND_COLOR_PRIMARY_2, player.name, playerName) 30 | } else { 31 | Component.translatable("death.attack.mob", BRAND_COLOR_PRIMARY_2, player.name, Component.translatable(src.source.entityType.registry().translationKey())) 32 | } 33 | } 34 | is EntityProjectileDamage -> { 35 | val playerName = (src.shooter as? Player)?.name 36 | if (playerName != null) { 37 | Component.translatable("death.attack.arrow", BRAND_COLOR_PRIMARY_2, player.name, playerName) 38 | } else if (src.shooter != null) { 39 | Component.translatable("death.attack.arrow", BRAND_COLOR_PRIMARY_2, player.name, Component.translatable(src.shooter!!.entityType.registry().translationKey())) 40 | } else { 41 | Component.translatable("death.attack.generic", BRAND_COLOR_PRIMARY_2, player.name) 42 | } 43 | } 44 | DamageType.OUT_OF_WORLD -> Component.translatable("death.attack.outOfWorld", BRAND_COLOR_PRIMARY_2, player.name) 45 | DamageType.FALL -> Component.translatable("death.attack.fall", BRAND_COLOR_PRIMARY_2, player.name) 46 | else -> Component.translatable("death.attack.generic", BRAND_COLOR_PRIMARY_2, player.name) 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/combat/EnumArmorToughness.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.combat 2 | 3 | import net.minestom.server.entity.LivingEntity 4 | import net.minestom.server.item.ItemStack 5 | import net.minestom.server.item.Material 6 | import kotlin.math.max 7 | 8 | enum class EnumArmorToughness(val armorToughness: Int, val defensePoints: Int, val material: Material) { 9 | 10 | TURTLE_SHELL(0, 2, Material.TURTLE_HELMET), 11 | LEATHER_HELMET(0, 1, Material.LEATHER_HELMET), 12 | GOLDEN_HELMET(0, 2, Material.GOLDEN_HELMET), 13 | CHAIN_HELMET(0, 2, Material.CHAINMAIL_HELMET), 14 | IRON_HELMET(0, 2, Material.IRON_HELMET), 15 | DIAMOND_HELMET(2, 3, Material.DIAMOND_HELMET), 16 | NETHERITE_HELMET(3, 3, Material.NETHERITE_HELMET), 17 | 18 | LEATHER_CHESTPLATE(0, 3, Material.LEATHER_CHESTPLATE), 19 | GOLDEN_CHESTPLATE(0, 5, Material.GOLDEN_CHESTPLATE), 20 | CHAINMAIL_CHESTPLATE(0, 5, Material.CHAINMAIL_CHESTPLATE), 21 | IRON_CHESTPLATE(0, 6, Material.IRON_CHESTPLATE), 22 | DIAMOND_CHESTPLATE(2, 8, Material.DIAMOND_CHESTPLATE), 23 | NETHERITE_CHESTPLATE(3, 8, Material.NETHERITE_CHESTPLATE), 24 | 25 | LEATHER_LEGGINGS(0, 2, Material.LEATHER_LEGGINGS), 26 | GOLDEN_LEGGINGS(0, 3, Material.GOLDEN_LEGGINGS), 27 | CHAINMAIL_LEGGINGS(0, 4, Material.CHAINMAIL_LEGGINGS), 28 | IRON_LEGGINGS(0, 5, Material.IRON_LEGGINGS), 29 | DIAMOND_LEGGINGS(2, 6, Material.DIAMOND_LEGGINGS), 30 | NETHERITE_LEGGINGS(3, 6, Material.NETHERITE_LEGGINGS), 31 | 32 | LEATHER_BOOTS(0, 1, Material.LEATHER_BOOTS), 33 | GOLDEN_BOOTS(0, 1, Material.GOLDEN_BOOTS), 34 | CHAINMAIL_BOOTS(0, 1, Material.CHAINMAIL_BOOTS), 35 | IRON_BOOTS(0, 2, Material.IRON_BOOTS), 36 | DIAMOND_BOOTS(2, 3, Material.DIAMOND_BOOTS), 37 | NETHERITE_BOOTS(3, 3, Material.NETHERITE_BOOTS); 38 | 39 | object ArmorToughness { 40 | 41 | private val armorDataMap = entries.associate { 42 | it.material to (it.armorToughness to it.defensePoints) 43 | } 44 | 45 | fun LivingEntity.getArmor() = listOf(helmet, chestplate, leggings, boots) 46 | private fun ItemStack.getArmorToughness() = armorDataMap[this.material()]?.first ?: 0 47 | private fun ItemStack.getDefensePoints() = armorDataMap[this.material()]?.second ?: 0 48 | 49 | fun getReducedDamage(incomingDamage: Double, target: LivingEntity): Double { 50 | val armor = target.getArmor() 51 | val armorDefense = armor.sumOf { it.getDefensePoints() } 52 | val armorToughness = armor.sumOf { it.getArmorToughness() } 53 | return getReducedDamage(incomingDamage, armorDefense, armorToughness) 54 | } 55 | 56 | /** 57 | * https://minecraft.wiki/w/Armor#Damage_reduction 58 | */ 59 | private fun getReducedDamage(incomingDamage: Double, armorDefense: Int, armorToughness: Int): Double { 60 | val percentDamageReduction = max( 61 | armorDefense / 5.0, 62 | armorDefense - ((4.0 * incomingDamage) / (armorToughness.toDouble().coerceAtMost(20.0) + 8.0)) 63 | ).coerceAtMost(20.0) / 25.0 64 | 65 | return incomingDamage * (1.0 - percentDamageReduction) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/BlockSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import net.minestom.server.instance.block.Block 4 | import org.spongepowered.configurate.serialize.ScalarSerializer 5 | import java.lang.reflect.Type 6 | import java.util.function.Predicate 7 | 8 | class BlockSerializer : ScalarSerializer(Block::class.java) { 9 | override fun deserialize(type: Type?, obj: Any?): Block? { 10 | val string = obj.toString() 11 | return Block.fromKey(string) 12 | } 13 | 14 | override fun serialize(item: Block?, typeSupported: Predicate>?): Any? { 15 | return item?.key()?.asString() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/ColorSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import net.minestom.server.color.Color 4 | import org.spongepowered.configurate.serialize.ScalarSerializer 5 | import java.lang.reflect.Type 6 | import java.util.function.Predicate 7 | 8 | class ColorSerializer : ScalarSerializer(Color::class.java) { 9 | override fun deserialize(type: Type?, obj: Any?): Color { 10 | val string = obj.toString() 11 | val split = string.split(",").map { it.trim().toInt() } 12 | return when (split.size) { 13 | 3 -> Color(split[0], split[1], split[2]) 14 | else -> error("Invalid number of elements: ${split.size}: expected 3") 15 | } 16 | } 17 | 18 | override fun serialize(item: Color?, typeSupported: Predicate>?): Any { 19 | return item?.run { "${red()},${green()},${blue()}" } ?: error("Cannot serialize null Color") 20 | } 21 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/ComponentSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import com.bluedragonmc.server.utils.miniMessage 4 | import net.kyori.adventure.text.Component 5 | import org.spongepowered.configurate.serialize.ScalarSerializer 6 | import java.lang.reflect.Type 7 | import java.util.function.Predicate 8 | 9 | class ComponentSerializer : ScalarSerializer(Component::class.java) { 10 | override fun deserialize(type: Type?, obj: Any?): Component { 11 | val string = obj.toString() 12 | return miniMessage.deserialize(string) 13 | } 14 | 15 | override fun serialize(item: Component?, typeSupported: Predicate>?): Any { 16 | return miniMessage.serialize(item ?: Component.empty()) 17 | } 18 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/EnchantmentListSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import io.leangen.geantyref.TypeToken 4 | import net.kyori.adventure.key.Key 5 | import net.minestom.server.MinecraftServer 6 | import net.minestom.server.item.component.EnchantmentList 7 | import net.minestom.server.item.enchant.Enchantment 8 | import net.minestom.server.registry.RegistryKey 9 | import org.spongepowered.configurate.ConfigurationNode 10 | import org.spongepowered.configurate.serialize.TypeSerializer 11 | import java.lang.reflect.Type 12 | 13 | class EnchantmentListSerializer : TypeSerializer { 14 | override fun deserialize(type: Type?, node: ConfigurationNode): EnchantmentList { 15 | val childrenMap = node.childrenMap() 16 | val newMap = mutableMapOf, Int>() 17 | for ((key, value) in childrenMap) { 18 | val registryKey = MinecraftServer.getEnchantmentRegistry().getKey(Key.key(key.toString())) 19 | ?: error("Unknown enchantment: \"$key\"") 20 | newMap[registryKey] = value.int 21 | } 22 | return EnchantmentList(newMap) 23 | } 24 | 25 | override fun serialize(type: Type?, obj: EnchantmentList?, node: ConfigurationNode?) { 26 | 27 | val pairs = obj?.enchantments?.entries?.map { entry -> 28 | entry.key.toString() to entry.value 29 | } ?: emptyList() 30 | 31 | val map = mapOf(*pairs.toTypedArray()) 32 | 33 | node?.set(object : TypeToken>() {}.type, map) 34 | } 35 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/EntityTypeSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import net.minestom.server.entity.EntityType 4 | import org.spongepowered.configurate.serialize.ScalarSerializer 5 | import java.lang.reflect.Type 6 | import java.util.function.Predicate 7 | 8 | class EntityTypeSerializer : ScalarSerializer(EntityType::class.java) { 9 | override fun deserialize(type: Type?, obj: Any?): EntityType { 10 | val string = obj.toString() 11 | return EntityType.fromKey(string) ?: error("Unknown entity type: \"$string\"") 12 | } 13 | 14 | override fun serialize(item: EntityType?, typeSupported: Predicate>?): Any? { 15 | return item?.key()?.asString() 16 | } 17 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/ItemStackSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import com.bluedragonmc.server.utils.noItalic 4 | import net.kyori.adventure.text.Component 5 | import net.minestom.server.color.Color 6 | import net.minestom.server.component.DataComponents 7 | import net.minestom.server.item.ItemStack 8 | import net.minestom.server.item.Material 9 | import net.minestom.server.item.component.EnchantmentList 10 | import org.spongepowered.configurate.ConfigurationNode 11 | import org.spongepowered.configurate.kotlin.extensions.get 12 | import org.spongepowered.configurate.serialize.TypeSerializer 13 | import java.lang.reflect.Type 14 | 15 | class ItemStackSerializer : TypeSerializer { 16 | 17 | override fun deserialize(type: Type?, node: ConfigurationNode): ItemStack { 18 | val material = node.node("material").get() ?: error("No material present") 19 | val amount = node.node("amount").getInt(1) 20 | node.hasChild("enchants") 21 | 22 | val enchantments = node.node("enchants").get() 23 | 24 | val name = node.node("name").get()?.noItalic() 25 | val lore = node.node("lore").getList(Component::class.java) 26 | val dye = node.node("dye").get() 27 | 28 | val itemStack = ItemStack.builder(material).run { 29 | 30 | amount(amount) 31 | 32 | if (name != null) { 33 | set(DataComponents.ITEM_NAME, name) 34 | } 35 | 36 | if (lore != null) { 37 | set(DataComponents.LORE, lore) 38 | } 39 | 40 | if (enchantments != null && enchantments.enchantments().isNotEmpty()) { 41 | set(DataComponents.ENCHANTMENTS, enchantments) 42 | } 43 | 44 | build() 45 | } 46 | 47 | if (dye != null) { 48 | return itemStack.with(DataComponents.DYED_COLOR, dye) 49 | } 50 | 51 | return itemStack 52 | } 53 | 54 | override fun serialize(type: Type?, obj: ItemStack?, node: ConfigurationNode?) { 55 | node?.node("material")?.set(obj?.material()) 56 | node?.node("amount")?.set(obj?.amount()) 57 | node?.node("enchants")?.set(obj?.get(DataComponents.ENCHANTMENTS)?.enchantments) 58 | node?.node("name")?.set(obj?.get(DataComponents.ITEM_NAME)) 59 | node?.node("lore")?.set(obj?.get(DataComponents.LORE)) 60 | } 61 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/KitSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import com.bluedragonmc.server.module.minigame.KitsModule 4 | import com.bluedragonmc.server.utils.miniMessage 5 | import com.bluedragonmc.server.utils.noItalic 6 | import net.kyori.adventure.text.Component 7 | import net.minestom.server.item.ItemStack 8 | import net.minestom.server.item.Material 9 | import net.minestom.server.utils.inventory.PlayerInventoryUtils 10 | import org.spongepowered.configurate.ConfigurationNode 11 | import org.spongepowered.configurate.kotlin.extensions.get 12 | import org.spongepowered.configurate.serialize.TypeSerializer 13 | import java.lang.reflect.Type 14 | 15 | class KitSerializer : TypeSerializer { 16 | override fun deserialize(type: Type?, node: ConfigurationNode): KitsModule.Kit { 17 | val name = node.node("name").get()?.noItalic() ?: Component.empty() 18 | val description = miniMessage.deserialize( 19 | node.node("description").string?.replace("\\n", "\n") ?: "" 20 | ) 21 | val icon = node.node("icon").get() ?: Material.DIAMOND 22 | val items = node.node("items").get>()!!.map { (str, itemStack) -> 23 | val slot = str.toIntOrNull() ?: when (str) { 24 | "helmet" -> PlayerInventoryUtils.HELMET_SLOT 25 | "chestplate" -> PlayerInventoryUtils.CHESTPLATE_SLOT 26 | "leggings" -> PlayerInventoryUtils.LEGGINGS_SLOT 27 | "boots" -> PlayerInventoryUtils.BOOTS_SLOT 28 | else -> error("Unknown slot preset: $str") 29 | } 30 | return@map slot to itemStack 31 | } 32 | val abilities = node.node("abilities").getList(String::class.java) ?: emptyList() 33 | return KitsModule.Kit(name, description, icon, hashMapOf(*items.toTypedArray()), abilities) 34 | } 35 | 36 | override fun serialize(type: Type?, obj: KitsModule.Kit?, node: ConfigurationNode) { 37 | TODO("Serialization of KitsModule.Kit not implemented.") 38 | } 39 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/MaterialSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import net.minestom.server.item.Material 4 | import org.spongepowered.configurate.serialize.ScalarSerializer 5 | import java.lang.reflect.Type 6 | import java.util.function.Predicate 7 | 8 | class MaterialSerializer : ScalarSerializer(Material::class.java) { 9 | override fun deserialize(type: Type?, obj: Any?): Material? { 10 | val string = obj.toString() 11 | return Material.fromKey(string) 12 | } 13 | 14 | override fun serialize(item: Material?, typeSupported: Predicate>?): Any? { 15 | return item?.key()?.asString() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/PlayerSkinSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import net.minestom.server.entity.PlayerSkin 4 | import org.spongepowered.configurate.ConfigurationNode 5 | import org.spongepowered.configurate.serialize.TypeSerializer 6 | import java.lang.reflect.Type 7 | 8 | class PlayerSkinSerializer : TypeSerializer { 9 | override fun deserialize(type: Type?, node: ConfigurationNode?): PlayerSkin { 10 | val textures = node?.node("textures")?.string 11 | val signature = node?.node("signature")?.string 12 | return PlayerSkin(textures, signature) 13 | } 14 | 15 | override fun serialize(type: Type?, obj: PlayerSkin?, node: ConfigurationNode?) { 16 | node?.node("textures")?.set(obj?.textures()) 17 | node?.node("signature")?.set(obj?.signature()) 18 | } 19 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/config/serializer/PosSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.config.serializer 2 | 3 | import net.minestom.server.coordinate.Pos 4 | import org.spongepowered.configurate.serialize.ScalarSerializer 5 | import java.lang.reflect.Type 6 | import java.util.function.Predicate 7 | 8 | class PosSerializer : ScalarSerializer(Pos::class.java) { 9 | override fun deserialize(type: Type?, obj: Any?): Pos { 10 | val string = obj.toString() 11 | val split = string.split(",").map { it.trim().toDouble() } 12 | return when (split.size) { 13 | 3 -> Pos(split[0], split[1], split[2]) 14 | 5 -> Pos(split[0], split[1], split[2], split[3].toFloat(), split[4].toFloat()) 15 | else -> error("Invalid number of elements: ${split.size}: expected 3 or 5") 16 | } 17 | } 18 | 19 | override fun serialize(item: Pos?, typeSupported: Predicate>?): Any { 20 | return item?.run { "$x,$y,$z,$yaw,$pitch" } ?: error("Cannot serialize null Pos") 21 | } 22 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/database/AwardsModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.database 2 | 3 | import com.bluedragonmc.server.* 4 | import com.bluedragonmc.server.model.PlayerDocument 5 | import com.bluedragonmc.server.service.Database 6 | import com.bluedragonmc.server.module.GameModule 7 | import com.bluedragonmc.server.service.Messaging 8 | import com.bluedragonmc.server.utils.* 9 | import kotlinx.coroutines.launch 10 | import net.kyori.adventure.sound.Sound 11 | import net.kyori.adventure.text.Component 12 | import net.kyori.adventure.text.format.TextDecoration 13 | import net.kyori.adventure.title.Title 14 | import net.minestom.server.MinecraftServer 15 | import net.minestom.server.entity.Player 16 | import net.minestom.server.event.Event 17 | import net.minestom.server.event.EventNode 18 | import net.minestom.server.sound.SoundEvent 19 | import java.time.Duration 20 | 21 | class AwardsModule : GameModule() { 22 | 23 | private lateinit var parent: Game 24 | 25 | override fun initialize(parent: Game, eventNode: EventNode) { 26 | this.parent = parent 27 | } 28 | 29 | fun awardCoins(player: Player, amount: Int, reason: Component) { 30 | player as CustomPlayer 31 | require(player.isDataInitialized()) { "Player's data has not loaded!" } 32 | val oldLevel = CustomPlayer.getXpLevel(player.data.experience).toInt() 33 | Database.IO.launch { 34 | player.data.compute(PlayerDocument::coins) { it + amount } 35 | player.data.compute(PlayerDocument::experience) { it + amount } 36 | Messaging.outgoing.recordCoinAward(player.uuid, amount, parent.id) 37 | val newLevel = CustomPlayer.getXpLevel(player.data.experience).toInt() 38 | if (newLevel > oldLevel) 39 | MinecraftServer.getSchedulerManager().buildTask { notifyLevelUp(player, oldLevel, newLevel) } 40 | .delay(Duration.ofSeconds(2)).schedule() 41 | } 42 | player.sendMessage( 43 | Component.translatable("module.award.awarded_coins", ALT_COLOR_2, Component.text(amount), reason) 44 | ) 45 | } 46 | 47 | private fun notifyLevelUp(player: CustomPlayer, oldLevel: Int, newLevel: Int) { 48 | player.showTitle( 49 | Title.title( 50 | Component.text("LEVEL UP!").withGradient(ALT_COLOR_1, ALT_COLOR_2).withDecoration(TextDecoration.BOLD), 51 | Component.text("You are now level ", ALT_COLOR_1) + Component.text(newLevel) 52 | ) 53 | ) 54 | val msg = buildComponent { 55 | +Component.translatable("module.award.level_up.1", ALT_COLOR_1) 56 | +Component.newline() 57 | +Component.translatable("module.award.level_up.2", Component.text(oldLevel, ALT_COLOR_2), Component.text(newLevel, ALT_COLOR_2)) 58 | } 59 | player.sendMessage(msg.surroundWithSeparators()) 60 | player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.PLAYER, 1.0F, 1.0F)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/database/StatRecorders.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.database 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.event.PlayerKillPlayerEvent 5 | import com.bluedragonmc.server.module.minigame.WinModule 6 | import com.bluedragonmc.server.utils.GameState 7 | import net.minestom.server.event.player.PlayerDeathEvent 8 | 9 | object StatRecorders { 10 | /** 11 | * Increments a statistic when a player kills another player. 12 | * One is added to the attacker's "kills" statistic. 13 | */ 14 | val PLAYER_KILLS = StatisticsModule.EventStatisticRecorder(PlayerKillPlayerEvent::class.java) { game, event -> 15 | incrementStatistic(event.attacker, getStatPrefix(game) + "_kills") 16 | } 17 | 18 | /** 19 | * Increments a statistic when a player dies by any cause. 20 | */ 21 | val PLAYER_DEATHS_ALL = StatisticsModule.EventStatisticRecorder(PlayerDeathEvent::class.java) { game, event -> 22 | if (game.state == GameState.INGAME) { 23 | incrementStatistic(event.player, getStatPrefix(game) + "_deaths") 24 | } 25 | } 26 | 27 | /** 28 | * Increments a statistic when a player dies because another player killed them in combat. 29 | * One is added to the target's "deaths_by_player" statistic. 30 | */ 31 | val PLAYER_DEATHS_BY_PLAYER = 32 | StatisticsModule.EventStatisticRecorder(PlayerKillPlayerEvent::class.java) { game, event -> 33 | incrementStatistic(event.target, getStatPrefix(game) + "_deaths_by_player") 34 | } 35 | 36 | /** 37 | * Combines the following: 38 | * [PLAYER_KILLS], [PLAYER_DEATHS_ALL], [PLAYER_DEATHS_BY_PLAYER] 39 | */ 40 | val KILLS_AND_DEATHS = 41 | StatisticsModule.MultiStatisticRecorder(PLAYER_KILLS, PLAYER_DEATHS_ALL, PLAYER_DEATHS_BY_PLAYER) 42 | 43 | /** 44 | * Records a "wins" and a "losses" statistic when a winner is declared. 45 | */ 46 | val WINS_AND_LOSSES = 47 | StatisticsModule.EventStatisticRecorder(WinModule.WinnerDeclaredEvent::class.java) { game, event -> 48 | ArrayList(game.players).forEach { player -> 49 | if (player in event.winningTeam.players) { 50 | incrementStatistic(player, getStatPrefix(game) + "_wins") 51 | } else { 52 | incrementStatistic(player, getStatPrefix(game) + "_losses") 53 | } 54 | } 55 | } 56 | 57 | val ALL = StatisticsModule.MultiStatisticRecorder( 58 | KILLS_AND_DEATHS, WINS_AND_LOSSES 59 | ) 60 | 61 | private fun getStatPrefix(game: Game): String { 62 | return if (game.mode.isNullOrBlank()) "game_${game.name.lowercase()}" 63 | else "game_${game.name.lowercase()}_${game.mode.lowercase()}" 64 | } 65 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/gameplay/ActionBarModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.gameplay 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.GameModule 5 | import net.kyori.adventure.text.Component 6 | import net.kyori.adventure.text.JoinConfiguration 7 | import net.kyori.adventure.text.format.NamedTextColor 8 | import net.minestom.server.entity.Player 9 | import net.minestom.server.event.Event 10 | import net.minestom.server.event.EventNode 11 | import net.minestom.server.event.player.PlayerTickEvent 12 | import net.minestom.server.event.trait.PlayerEvent 13 | 14 | class ActionBarModule( 15 | private val interval: Int = 2, 16 | private val separator: Component = Component.text(" | ", NamedTextColor.DARK_GRAY), 17 | ) : GameModule() { 18 | override fun initialize(parent: Game, eventNode: EventNode) { 19 | eventNode.addListener(PlayerTickEvent::class.java) { event -> 20 | if (event.entity.aliveTicks % interval == 0L) { 21 | val actionBar = collectActionBar(parent, event.player) 22 | event.player.sendActionBar(actionBar) 23 | } 24 | } 25 | } 26 | 27 | private fun collectActionBar(parent: Game, player: Player): Component { 28 | val event = CollectActionBarEvent(player) 29 | parent.callEvent(event) 30 | val items = event.getItems() 31 | if (items.isEmpty()) return Component.empty() 32 | return Component.join(JoinConfiguration.separator(separator), items) 33 | } 34 | 35 | data class CollectActionBarEvent( 36 | private val player: Player, 37 | private val items: MutableList = mutableListOf(), 38 | ) : PlayerEvent { 39 | override fun getPlayer(): Player = player 40 | 41 | fun addItem(item: Component) { 42 | items.add(item) 43 | } 44 | 45 | fun getItems() = items.toList() 46 | } 47 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/gameplay/ChestLootModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.gameplay 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.event.ChestPopulateEvent 5 | import com.bluedragonmc.server.module.DependsOn 6 | import com.bluedragonmc.server.module.GameModule 7 | import com.bluedragonmc.server.module.vanilla.ChestModule 8 | import net.minestom.server.coordinate.Point 9 | import net.minestom.server.event.Event 10 | import net.minestom.server.event.EventNode 11 | import net.minestom.server.item.ItemStack 12 | import kotlin.random.Random 13 | 14 | @DependsOn(ChestModule::class) 15 | class ChestLootModule(private val lootProvider: ChestLootProvider) : GameModule() { 16 | 17 | interface ChestLootProvider { 18 | fun getLoot(chestLocation: Point): Collection 19 | } 20 | 21 | /** 22 | * Provides loot using a different provider based on the chest's location. 23 | * This can be used to give a separate list of items based on where the chest is located. 24 | * For example, SkyWars contains chests near the middle of the map which contain higher-value items. 25 | */ 26 | class LocationBasedLootProvider(private val provider: (Point) -> ChestLootProvider) : ChestLootProvider { 27 | override fun getLoot(chestLocation: Point): Collection = 28 | provider(chestLocation).getLoot(chestLocation) 29 | } 30 | 31 | /** 32 | * Provides loot based on the [potentialItems]. Each item, if selected, is given a slot in the chest. 33 | * If a slot is already taken, the item will not be set or assigned a new slot. 34 | */ 35 | class RandomLootProvider(private val potentialItems: Collection) : ChestLootProvider { 36 | data class WeightedItemStack(val itemStack: ItemStack, val chance: Float) 37 | 38 | override fun getLoot(chestLocation: Point): Collection { 39 | val slots = MutableList(27) { ItemStack.AIR } 40 | potentialItems.filter { Random.nextFloat() <= it.chance }.forEach { 41 | // Find a slot for this item that has not been taken yet 42 | var iters = 0 43 | while (iters < 10) { 44 | val slot = Random.nextInt(slots.size) 45 | if (slots[slot] === ItemStack.AIR) { 46 | slots[slot] = it.itemStack 47 | break 48 | } 49 | iters ++ 50 | } 51 | } 52 | return slots 53 | } 54 | } 55 | 56 | override fun initialize(parent: Game, eventNode: EventNode) { 57 | eventNode.addListener(ChestPopulateEvent::class.java) { event -> 58 | lootProvider.getLoot(event.position).forEachIndexed { index, itemStack -> 59 | event.menu.setItemStack(event.player, index, itemStack) 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/gameplay/InstantRespawnModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.gameplay 2 | 3 | import com.bluedragonmc.server.CustomPlayer 4 | import com.bluedragonmc.server.Game 5 | import com.bluedragonmc.server.module.GameModule 6 | import net.minestom.server.MinecraftServer 7 | import net.minestom.server.coordinate.Vec 8 | import net.minestom.server.entity.EntityPose 9 | import net.minestom.server.entity.Player 10 | import net.minestom.server.event.Event 11 | import net.minestom.server.event.EventDispatcher 12 | import net.minestom.server.event.EventNode 13 | import net.minestom.server.event.entity.EntityDamageEvent 14 | import net.minestom.server.event.player.PlayerDeathEvent 15 | import net.minestom.server.event.player.PlayerRespawnEvent 16 | 17 | /** 18 | * This module automatically respawns players when they die. 19 | * No configuration is necessary. 20 | */ 21 | class InstantRespawnModule : GameModule() { 22 | override fun initialize(parent: Game, eventNode: EventNode) { 23 | eventNode.addListener(EntityDamageEvent::class.java) { event -> 24 | if (event.entity is Player && event.damage.amount >= (event.entity.health + (event.entity as Player).additionalHearts)) { 25 | event.damage.amount = 0.0f 26 | 27 | (event.entity as CustomPlayer).apply { 28 | refreshHealth() 29 | 30 | MinecraftServer.getGlobalEventHandler().call( 31 | PlayerDeathEvent( 32 | this, 33 | lastDamageSource?.buildDeathScreenText(this), 34 | lastDamageSource?.buildDeathMessage(this), 35 | ) 36 | ) 37 | 38 | isDead = true 39 | fireTicks = 0 40 | pose = EntityPose.STANDING 41 | velocity = Vec.ZERO 42 | 43 | val respawnEvent = PlayerRespawnEvent(this) 44 | EventDispatcher.call(respawnEvent) 45 | teleport(respawnEvent.respawnPosition).thenRun { refreshAfterTeleport() } 46 | 47 | MinecraftServer.getSchedulerManager().scheduleNextTick { 48 | isDead = false 49 | } 50 | } 51 | } 52 | }.priority = -1 53 | } 54 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/gameplay/InventoryPermissionsModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.gameplay 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.GameModule 5 | import net.minestom.server.event.Event 6 | import net.minestom.server.event.EventNode 7 | import net.minestom.server.event.inventory.InventoryPreClickEvent 8 | import net.minestom.server.event.item.ItemDropEvent 9 | import net.minestom.server.event.player.PlayerSwapItemEvent 10 | import net.minestom.server.inventory.PlayerInventory 11 | 12 | /** 13 | * A module to manage the player's ability to edit their inventory. 14 | */ 15 | class InventoryPermissionsModule(var allowDropItem: Boolean, var allowMoveItem: Boolean) : GameModule() { 16 | override fun initialize(parent: Game, eventNode: EventNode) { 17 | eventNode.addListener(InventoryPreClickEvent::class.java) { event -> 18 | if (event.inventory !is PlayerInventory) return@addListener 19 | event.isCancelled = !allowMoveItem 20 | } 21 | eventNode.addListener(ItemDropEvent::class.java) { event -> 22 | event.isCancelled = !allowDropItem 23 | } 24 | eventNode.addListener(PlayerSwapItemEvent::class.java) { event -> 25 | event.isCancelled = !allowMoveItem 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/gameplay/MaxHealthModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.gameplay 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.event.PlayerLeaveGameEvent 5 | import com.bluedragonmc.server.module.GameModule 6 | import net.minestom.server.entity.attribute.Attribute 7 | import net.minestom.server.event.Event 8 | import net.minestom.server.event.EventNode 9 | import net.minestom.server.event.player.PlayerSpawnEvent 10 | 11 | /** 12 | * Sets the max health of the player when they join the instance. 13 | * This sets the base value of the attribute, it does not add a modifier. 14 | * The module automatically resets their max health to 20 when they leave the instance. 15 | */ 16 | class MaxHealthModule(private val maxHealth: Double) : GameModule() { 17 | 18 | override fun initialize(parent: Game, eventNode: EventNode) { 19 | eventNode.addListener(PlayerSpawnEvent::class.java) { event -> 20 | event.player.getAttribute(Attribute.MAX_HEALTH).baseValue = maxHealth 21 | } 22 | eventNode.addListener(PlayerLeaveGameEvent::class.java) { event -> 23 | event.player.getAttribute(Attribute.MAX_HEALTH).baseValue = 20.0 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/instance/CustomGeneratorInstanceModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.instance 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.NAMESPACE 5 | import net.kyori.adventure.key.Key 6 | import net.minestom.server.MinecraftServer 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.event.Event 9 | import net.minestom.server.event.EventNode 10 | import net.minestom.server.instance.Instance 11 | import net.minestom.server.instance.generator.Generator 12 | import net.minestom.server.registry.RegistryKey 13 | import net.minestom.server.world.DimensionType 14 | 15 | class CustomGeneratorInstanceModule( 16 | private val dimensionType: RegistryKey = DimensionType.OVERWORLD, 17 | private val generator: Generator, 18 | ) : InstanceModule() { 19 | private lateinit var instance: Instance 20 | 21 | override fun initialize(parent: Game, eventNode: EventNode) { 22 | instance = MinecraftServer.getInstanceManager().createInstanceContainer(dimensionType) 23 | instance.setGenerator(generator) 24 | } 25 | 26 | override fun ownsInstance(instance: Instance): Boolean { 27 | return instance == this.instance 28 | } 29 | 30 | override fun getSpawningInstance(player: Player): Instance = instance 31 | 32 | fun getInstance() = instance 33 | 34 | companion object { 35 | 36 | private lateinit var key: RegistryKey 37 | 38 | init { 39 | val id = Key.key("$NAMESPACE:fullbright_dimension") 40 | if (MinecraftServer.getDimensionTypeRegistry().get(id) == null) { 41 | key = MinecraftServer.getDimensionTypeRegistry().register( 42 | id, 43 | DimensionType.builder().ambientLight(1.0F).build() 44 | ) 45 | } 46 | } 47 | 48 | fun getFullbrightDimension() = key 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/instance/InstanceContainerModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.instance 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.DependsOn 5 | import com.bluedragonmc.server.module.map.AnvilFileMapProviderModule 6 | import net.minestom.server.MinecraftServer 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.event.Event 9 | import net.minestom.server.event.EventNode 10 | import net.minestom.server.instance.Instance 11 | import net.minestom.server.instance.InstanceContainer 12 | import net.minestom.server.instance.anvil.AnvilLoader 13 | 14 | @DependsOn(AnvilFileMapProviderModule::class) 15 | class InstanceContainerModule : InstanceModule() { 16 | 17 | private lateinit var instance: InstanceContainer 18 | 19 | override fun getSpawningInstance(player: Player): Instance = this.instance 20 | override fun ownsInstance(instance: Instance): Boolean = instance == this.instance 21 | 22 | override fun initialize(parent: Game, eventNode: EventNode) { 23 | // Create a copy of the loaded InstanceContainer to prevent modifying the state of the original 24 | this.instance = parent.getModule().instanceContainer.copy().apply { 25 | chunkLoader = AnvilLoader(parent.getModule().worldFolder) 26 | } 27 | MinecraftServer.getInstanceManager().registerInstance(instance) 28 | } 29 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/instance/InstanceModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.instance 2 | 3 | import com.bluedragonmc.server.module.GameModule 4 | import net.minestom.server.entity.Player 5 | import net.minestom.server.instance.Instance 6 | 7 | abstract class InstanceModule : GameModule() { 8 | 9 | /** 10 | * Get a set of instances that are required, but not owned, by this module. 11 | * This is necessary because shared instances must have a registered 12 | * instance container for chunk loading, but the instance container can be used 13 | * by multiple games at the same time (and therefore not "owned" by any of them) 14 | */ 15 | open fun getRequiredInstances(): Iterable { return emptySet() } 16 | 17 | /** 18 | * Get the instance that a player should spawn in when initially joining the game. 19 | */ 20 | abstract fun getSpawningInstance(player: Player): Instance 21 | 22 | /** 23 | * Determines whether the module "owns" an instance. Modules should own an instance 24 | * if they created it, and ownership should be released when the instance is no longer needed. 25 | * Instances with no modules that declare ownership of them may be cleaned up at any time. 26 | */ 27 | abstract fun ownsInstance(instance: Instance): Boolean 28 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/instance/InstanceTimeModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.instance 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.DependsOn 5 | import com.bluedragonmc.server.module.GameModule 6 | import com.bluedragonmc.server.module.config.ConfigModule 7 | import net.minestom.server.event.Event 8 | import net.minestom.server.event.EventNode 9 | 10 | /** 11 | * Uses a config value (if present) to set the time of day/night in the instance 12 | */ 13 | @DependsOn(ConfigModule::class) 14 | class InstanceTimeModule(val default: Int = 12000) : GameModule() { 15 | override fun initialize(parent: Game, eventNode: EventNode) { 16 | val time = parent.getModule().getConfig().node("world", "time").getInt(default) 17 | parent.getOwnedInstances().forEach { 18 | it.time = time.toLong() 19 | it.timeRate = 0 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/instance/SharedInstanceModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.instance 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.DependsOn 5 | import com.bluedragonmc.server.module.map.AnvilFileMapProviderModule 6 | import net.minestom.server.MinecraftServer 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.event.Event 9 | import net.minestom.server.event.EventNode 10 | import net.minestom.server.instance.Instance 11 | import net.minestom.server.instance.InstanceContainer 12 | import net.minestom.server.instance.SharedInstance 13 | 14 | @DependsOn(AnvilFileMapProviderModule::class) 15 | class SharedInstanceModule : InstanceModule() { 16 | 17 | private lateinit var instanceContainer: InstanceContainer 18 | private lateinit var instance: SharedInstance 19 | 20 | override fun getSpawningInstance(player: Player): Instance = this.instance 21 | override fun ownsInstance(instance: Instance): Boolean = instance == this.instance 22 | 23 | fun getInstance() = instance 24 | 25 | override fun getRequiredInstances(): Iterable { 26 | return setOf(instanceContainer) 27 | } 28 | 29 | override fun initialize(parent: Game, eventNode: EventNode) { 30 | instanceContainer = parent.getModule().instanceContainer 31 | if (!instanceContainer.isRegistered) { 32 | MinecraftServer.getInstanceManager().registerInstance(instanceContainer) 33 | } 34 | instance = MinecraftServer.getInstanceManager().createSharedInstance(instanceContainer) 35 | } 36 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/map/AnvilFileMapProviderModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.map 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.GameModule 5 | import net.minestom.server.MinecraftServer 6 | import net.minestom.server.event.Event 7 | import net.minestom.server.event.EventNode 8 | import net.minestom.server.event.instance.InstanceUnregisterEvent 9 | import net.minestom.server.instance.DynamicChunk 10 | import net.minestom.server.instance.InstanceContainer 11 | import net.minestom.server.instance.LightingChunk 12 | import net.minestom.server.instance.anvil.AnvilLoader 13 | import net.minestom.server.registry.RegistryKey 14 | import net.minestom.server.tag.Tag 15 | import net.minestom.server.world.DimensionType 16 | import java.nio.file.Path 17 | import kotlin.io.path.absolutePathString 18 | 19 | /** 20 | * Supplies an [InstanceContainer] to the [com.bluedragonmc.server.module.instance.InstanceContainerModule]. 21 | * 22 | * **Note**: By default, [lighting](https://wiki.minestom.net/world/chunk-management/lightloader) is enabled by setting the chunk supplier to the [LightingChunk] constructor. 23 | * To disable this, you can revert it back to [DynamicChunk] like so: 24 | * ```kotlin 25 | * use(AnvilFileMapProviderModule(path)) { module -> 26 | * module.getInstance().setChunkSupplier(::DynamicChunk) 27 | * } 28 | * ``` 29 | * 30 | * [See Documentation](https://developer.bluedragonmc.com/modules/anvilfilemapprovidermodule/) 31 | */ 32 | class AnvilFileMapProviderModule(val worldFolder: Path, private val dimensionType: RegistryKey = DimensionType.OVERWORLD) : GameModule() { 33 | 34 | lateinit var instanceContainer: InstanceContainer 35 | private set 36 | 37 | override fun initialize(parent: Game, eventNode: EventNode) { 38 | // If this world has already been loaded, use its existing InstanceContainer 39 | if (loadedMaps.containsKey(worldFolder)) { 40 | instanceContainer = loadedMaps[worldFolder]!! 41 | return 42 | } 43 | 44 | // If not, create a new InstanceContainer 45 | instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer(dimensionType) 46 | instanceContainer.chunkLoader = AnvilLoader(worldFolder) 47 | instanceContainer.setChunkSupplier(::LightingChunk) 48 | instanceContainer.setTag(MAP_NAME_TAG, worldFolder.absolutePathString()) 49 | 50 | loadedMaps[worldFolder] = instanceContainer 51 | } 52 | 53 | companion object { 54 | val loadedMaps = mutableMapOf() 55 | val MAP_NAME_TAG = Tag.String("anvil_file_map_name") 56 | 57 | init { 58 | // If an InstanceContainer is unregistered, remove it from `loadedMaps` so it can be garbage collected 59 | MinecraftServer.getGlobalEventHandler().addListener(InstanceUnregisterEvent::class.java) { event -> 60 | loadedMaps.entries.removeIf { (_, instance) -> instance == event.instance } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/minigame/MOTDModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.minigame 2 | 3 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_1 4 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_2 5 | import com.bluedragonmc.server.Game 6 | import com.bluedragonmc.server.module.GameModule 7 | import com.bluedragonmc.server.module.SoftDependsOn 8 | import com.bluedragonmc.server.module.config.ConfigModule 9 | import com.bluedragonmc.server.utils.buildComponent 10 | import com.bluedragonmc.server.utils.noBold 11 | import com.bluedragonmc.server.utils.plus 12 | import com.bluedragonmc.server.utils.surroundWithSeparators 13 | import net.kyori.adventure.text.Component 14 | import net.kyori.adventure.text.event.HoverEvent 15 | import net.kyori.adventure.text.format.NamedTextColor 16 | import net.kyori.adventure.text.format.TextDecoration 17 | import net.minestom.server.event.Event 18 | import net.minestom.server.event.EventNode 19 | import net.minestom.server.event.player.PlayerSpawnEvent 20 | 21 | /** 22 | * Displays a message to players when they join the game. 23 | */ 24 | @SoftDependsOn(ConfigModule::class) 25 | class MOTDModule(private val motd: Component, private var showMapName: Boolean = true) : GameModule() { 26 | 27 | override fun initialize(parent: Game, eventNode: EventNode) { 28 | val node = parent.getModuleOrNull()?.getConfig()?.node("world") 29 | 30 | if (!parent.hasModule()) { 31 | showMapName = false 32 | } 33 | 34 | val name = node?.node("name")?.string ?: "Untitled" 35 | val description = node?.node("description")?.string ?: "An awesome map!" 36 | val author = node?.node("author")?.string ?: "BlueDragon Build Team" 37 | 38 | eventNode.addListener(PlayerSpawnEvent::class.java) { event -> 39 | event.player.sendMessage( 40 | buildComponent { 41 | // Game name 42 | +Component.text(parent.name, BRAND_COLOR_PRIMARY_1, TextDecoration.BOLD) 43 | +Component.newline() 44 | +buildComponent { 45 | // MOTD 46 | +motd.color(NamedTextColor.WHITE) 47 | if (showMapName) +Component.newline() 48 | if (showMapName) +Component.translatable( 49 | "module.motd.map", 50 | BRAND_COLOR_PRIMARY_2, 51 | // Map name 52 | Component.text(name, BRAND_COLOR_PRIMARY_1, TextDecoration.BOLD) 53 | .hoverEvent( 54 | HoverEvent.showText( 55 | Component.text( 56 | name, 57 | BRAND_COLOR_PRIMARY_1, 58 | TextDecoration.BOLD 59 | ) + Component.newline() + Component.text( 60 | description, 61 | NamedTextColor.GRAY 62 | ).noBold() 63 | ) 64 | ), 65 | // Map builder 66 | Component.text(author, BRAND_COLOR_PRIMARY_1) 67 | ) 68 | }.noBold() 69 | }.surroundWithSeparators() 70 | ) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/minigame/PlayerResetModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.minigame 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.GameModule 5 | import net.kyori.adventure.nbt.CompoundBinaryTag 6 | import net.minestom.server.entity.GameMode 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.entity.attribute.Attribute 9 | import net.minestom.server.event.Event 10 | import net.minestom.server.event.EventNode 11 | import net.minestom.server.event.player.PlayerSpawnEvent 12 | 13 | /** 14 | * "Resets" the player when they join the game. This changes some basic attributes to make sure effects don't persist in between games. 15 | * - Change game mode 16 | * - Clear inventory 17 | * - Reset health/hunger 18 | * - Reset movement speed 19 | * - Clear potion effects 20 | * - Disable flying 21 | * - Stop fire damage 22 | * - Disable glowing 23 | * - Reset XP 24 | * - Clear all tags 25 | */ 26 | class PlayerResetModule(val defaultGameMode: GameMode? = null) : GameModule() { 27 | override fun initialize(parent: Game, eventNode: EventNode) { 28 | eventNode.addListener(PlayerSpawnEvent::class.java) { event -> 29 | resetPlayer(event.player, defaultGameMode) 30 | } 31 | } 32 | 33 | fun resetPlayer(player: Player, gameMode: GameMode? = defaultGameMode) { 34 | player.gameMode = gameMode ?: player.gameMode 35 | player.inventory.clear() 36 | Attribute.values().forEach { attribute -> 37 | player.getAttribute(attribute).modifiers().forEach { modifier -> 38 | player.getAttribute(attribute).removeModifier(modifier) 39 | } 40 | } 41 | player.health = player.getAttribute(Attribute.MAX_HEALTH).value.toFloat() 42 | player.food = 20 43 | player.clearEffects() 44 | player.fireTicks = 0 45 | player.isGlowing = false 46 | player.isAllowFlying = false 47 | player.level = 0 48 | player.exp = 0F 49 | player.tagHandler().updateContent(CompoundBinaryTag.empty()) 50 | player.stopSpectating() 51 | } 52 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/minigame/TimedRespawnModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.minigame 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.event.GameEvent 5 | import com.bluedragonmc.server.module.GameModule 6 | import com.bluedragonmc.server.utils.manage 7 | import net.kyori.adventure.text.Component 8 | import net.kyori.adventure.text.format.NamedTextColor 9 | import net.kyori.adventure.title.Title 10 | import net.minestom.server.MinecraftServer 11 | import net.minestom.server.entity.GameMode 12 | import net.minestom.server.entity.Player 13 | import net.minestom.server.event.Event 14 | import net.minestom.server.event.EventNode 15 | import net.minestom.server.event.player.PlayerDeathEvent 16 | import java.time.Duration 17 | 18 | class TimedRespawnModule(private val seconds: Int = 5) : GameModule() { 19 | override fun initialize(parent: Game, eventNode: EventNode) { 20 | eventNode.addListener(PlayerDeathEvent::class.java) { event -> 21 | MinecraftServer.getSchedulerManager().buildTask { 22 | if (parent.getModule().isSpectating(event.player)) return@buildTask 23 | event.player.respawn() 24 | event.player.gameMode = GameMode.SPECTATOR 25 | event.player.showTitle( 26 | Title.title( 27 | Component.translatable("module.respawn.title", NamedTextColor.RED), 28 | Component.translatable("module.respawn.subtitle", NamedTextColor.RED, Component.text(seconds)) 29 | ) 30 | ) 31 | MinecraftServer.getSchedulerManager().buildTask { 32 | parent.callEvent(TimedRespawnEvent(parent, event.player)) 33 | if (parent.hasModule()) { 34 | val mode = parent.getModule().defaultGameMode 35 | if (mode != null) event.player.gameMode = mode 36 | } 37 | event.player.teleport(event.player.respawnPoint) 38 | }.delay(Duration.ofSeconds(seconds.toLong())).schedule().manage(parent) 39 | }.delay(Duration.ofMillis(20)).schedule().manage(parent) 40 | } 41 | } 42 | 43 | class TimedRespawnEvent(game: Game, val player: Player) : GameEvent(game) 44 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/minigame/VoidDeathModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.minigame 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.GameModule 5 | import net.minestom.server.entity.damage.DamageType 6 | import net.minestom.server.event.Event 7 | import net.minestom.server.event.EventNode 8 | import net.minestom.server.event.player.PlayerDeathEvent 9 | import net.minestom.server.event.player.PlayerMoveEvent 10 | 11 | /** 12 | * A module that respawns the player when their height goes below a certain threshold. 13 | * @param threshold Minimum height the player can be at without being respawned. 14 | * @param respawnMode True if the player should just be respawned instead of being killed. 15 | */ 16 | class VoidDeathModule(private val threshold: Double, private val respawnMode: Boolean = false) : GameModule() { 17 | override fun initialize(parent: Game, eventNode: EventNode) { 18 | eventNode.addListener(PlayerMoveEvent::class.java) { event -> 19 | if (event.player.position.y < threshold && !event.player.isDead) { 20 | if (respawnMode) { 21 | event.player.respawn() 22 | event.player.teleport(event.player.respawnPoint) 23 | parent.callEvent(PlayerDeathEvent(event.player, null, null)) 24 | } else { 25 | event.player.damage(DamageType.OUT_OF_WORLD, Float.MAX_VALUE) 26 | } 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/vanilla/FireworkRocketModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.vanilla 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.GameModule 5 | import net.kyori.adventure.sound.Sound 6 | import net.minestom.server.MinecraftServer 7 | import net.minestom.server.entity.Entity 8 | import net.minestom.server.entity.EntityType 9 | import net.minestom.server.entity.Player 10 | import net.minestom.server.entity.metadata.projectile.FireworkRocketMeta 11 | import net.minestom.server.event.Event 12 | import net.minestom.server.event.EventNode 13 | import net.minestom.server.event.player.PlayerUseItemEvent 14 | import net.minestom.server.item.Material 15 | import net.minestom.server.network.packet.server.play.EntityStatusPacket 16 | import net.minestom.server.sound.SoundEvent 17 | import net.minestom.server.utils.time.TimeUnit 18 | import java.time.Duration 19 | 20 | /** 21 | * Vanilla-like functionality for firework rockets. 22 | * The particles depend on the properties of the rocket. 23 | * If [boostElytra] is enabled, using a firework rocket will 24 | * increase the velocity of a player flying with elytra. 25 | */ 26 | class FireworkRocketModule(private val boostElytra: Boolean = true) : GameModule() { 27 | override fun initialize(parent: Game, eventNode: EventNode) { 28 | eventNode.addListener(PlayerUseItemEvent::class.java) { event -> 29 | // Firework particles 30 | if (event.itemStack.material() != Material.FIREWORK_ROCKET) return@addListener 31 | val firework = Entity(EntityType.FIREWORK_ROCKET) 32 | (firework.entityMeta as FireworkRocketMeta).fireworkInfo = event.itemStack 33 | firework.setNoGravity(true) 34 | firework.setInstance(event.instance, event.player.position) 35 | event.instance.playSound( 36 | Sound.sound( 37 | SoundEvent.ENTITY_FIREWORK_ROCKET_LAUNCH, Sound.Source.PLAYER, 2.0f, 1.0f 38 | ), event.player.position 39 | ) 40 | MinecraftServer.getSchedulerManager().buildTask { 41 | event.instance.sendGroupedPacket(EntityStatusPacket(firework.entityId, 17)) 42 | firework.remove() 43 | }.delay(Duration.ofSeconds(1)).schedule() 44 | // Elytra boost 45 | if (boostElytra) { 46 | event.player.setItemInHand(event.hand, event.itemStack.withAmount(event.itemStack.amount()-1)) 47 | elytraBoostPlayer(event.player) 48 | } 49 | } 50 | } 51 | 52 | fun elytraBoostPlayer(player: Player) { 53 | val velocityTask = MinecraftServer.getSchedulerManager().buildTask { 54 | player.velocity = player.position.direction().mul(30.0) 55 | }.repeat(1, TimeUnit.SERVER_TICK).schedule() 56 | 57 | MinecraftServer.getSchedulerManager().buildTask { 58 | velocityTask.cancel() 59 | }.delay(30, TimeUnit.SERVER_TICK).schedule() 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/vanilla/ItemPickupModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.vanilla 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.GameModule 5 | import net.minestom.server.entity.GameMode 6 | import net.minestom.server.entity.Player 7 | import net.minestom.server.event.Event 8 | import net.minestom.server.event.EventNode 9 | import net.minestom.server.event.item.PickupItemEvent 10 | 11 | /** 12 | * Allows players to pick up items. 13 | */ 14 | class ItemPickupModule : GameModule() { 15 | override fun initialize(parent: Game, eventNode: EventNode) { 16 | eventNode.addListener(PickupItemEvent::class.java) { event -> 17 | val entity = event.entity 18 | if (entity !is Player) return@addListener 19 | if (entity.gameMode == GameMode.SPECTATOR) { 20 | event.isCancelled = true 21 | return@addListener 22 | } 23 | entity.inventory.addItemStack(event.itemStack) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/vanilla/NaturalRegenerationModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.vanilla 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.event.GameStartEvent 5 | import com.bluedragonmc.server.module.DependsOn 6 | import com.bluedragonmc.server.module.GameModule 7 | import com.bluedragonmc.server.module.combat.OldCombatModule 8 | import com.bluedragonmc.server.utils.manage 9 | import net.minestom.server.MinecraftServer 10 | import net.minestom.server.entity.Player 11 | import net.minestom.server.event.Event 12 | import net.minestom.server.event.EventNode 13 | import java.time.Duration 14 | 15 | /** 16 | * Regenerates a player's health by 0.5 every second when they have been out of combat for at least 15 seconds. 17 | */ 18 | @DependsOn(OldCombatModule::class) 19 | class NaturalRegenerationModule : GameModule() { 20 | 21 | private val combatStatus = hashMapOf() 22 | 23 | override fun initialize(parent: Game, eventNode: EventNode) { 24 | eventNode.addListener(OldCombatModule.PlayerAttackEvent::class.java) { event -> 25 | if (event.target !is Player) return@addListener 26 | combatStatus[event.attacker] = 0 27 | combatStatus[event.target] = 0 28 | } 29 | eventNode.addListener(GameStartEvent::class.java) { 30 | parent.players.forEach { combatStatus[it] = 0 } 31 | MinecraftServer.getSchedulerManager().buildTask { 32 | for (s in combatStatus) { 33 | combatStatus[s.key] = combatStatus.getOrDefault(s.key, 0) + 1 34 | if (combatStatus[s.key]!! >= 15) s.key.health += 0.5f 35 | } 36 | }.repeat(Duration.ofSeconds(1)).schedule().manage(parent) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/module/vanilla/PickItemModule.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.module.vanilla 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.module.GameModule 5 | import net.minestom.server.event.Event 6 | import net.minestom.server.event.EventNode 7 | import net.minestom.server.event.player.PlayerPickBlockEvent 8 | import net.minestom.server.instance.block.Block 9 | 10 | /** 11 | * Enables the vanilla "pick block" functionality (middle click) for survival mode 12 | */ 13 | class PickItemModule : GameModule() { 14 | override fun initialize( 15 | parent: Game, 16 | eventNode: EventNode 17 | ) { 18 | eventNode.addListener(PlayerPickBlockEvent::class.java) { event -> 19 | val block = event.instance.getBlock(event.blockPosition) 20 | if (event.player.inventory.getItemStack(event.player.heldSlot.toInt()).material().block()?.compare(block, Block.Comparator.ID) == true) { 21 | // If the player is already holding a matching block, do nothing 22 | return@addListener 23 | } 24 | for (slot in 0..8) { 25 | if (event.player.inventory.getItemStack(slot).material().block()?.compare(block, Block.Comparator.ID) == true) { 26 | event.player.setHeldItemSlot(slot.toByte()) 27 | return@addListener 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/service/Database.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.service 2 | 3 | import com.bluedragonmc.server.api.DatabaseConnection 4 | import kotlinx.coroutines.CoroutineName 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.SupervisorJob 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | object Database { 11 | 12 | lateinit var connection: DatabaseConnection 13 | private set 14 | 15 | fun initialize(connection: DatabaseConnection) { 16 | Database.connection = connection 17 | } 18 | 19 | val IO = object : CoroutineScope { 20 | override val coroutineContext: CoroutineContext = 21 | Dispatchers.IO + SupervisorJob() + CoroutineName("Database IO") 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/service/Messaging.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.service 2 | 3 | import com.bluedragonmc.server.api.IncomingRPCHandler 4 | import com.bluedragonmc.server.api.OutgoingRPCHandler 5 | import kotlinx.coroutines.CoroutineName 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.SupervisorJob 9 | import kotlin.coroutines.CoroutineContext 10 | 11 | object Messaging { 12 | 13 | lateinit var outgoing: OutgoingRPCHandler 14 | private set 15 | 16 | lateinit var incoming: IncomingRPCHandler 17 | private set 18 | 19 | fun initializeOutgoing(connection: OutgoingRPCHandler) { 20 | outgoing = connection 21 | } 22 | 23 | fun initializeIncoming(handler: IncomingRPCHandler) { 24 | incoming = handler 25 | } 26 | 27 | fun isConnected(): Boolean = 28 | Messaging::incoming.isInitialized && incoming.isConnected() && Messaging::outgoing.isInitialized && outgoing.isConnected() 29 | 30 | val IO = object : CoroutineScope { 31 | override val coroutineContext: CoroutineContext = 32 | Dispatchers.IO + SupervisorJob() + CoroutineName("Messaging IO") 33 | } 34 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/service/Permissions.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.service 2 | 3 | import com.bluedragonmc.server.api.PermissionManager 4 | import java.util.* 5 | 6 | object Permissions { 7 | 8 | lateinit var manager: PermissionManager 9 | private set 10 | 11 | fun initialize(manager: PermissionManager) { 12 | this.manager = manager 13 | } 14 | 15 | fun hasPermission(player: UUID, node: String) = manager.hasPermission(player, node) 16 | fun getMetadata(player: UUID) = manager.getMetadata(player) 17 | } 18 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/BlockUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | import net.minestom.server.instance.block.Block 4 | 5 | fun Block.isFullCube() = registry().collisionShape().run { 6 | val start = relativeStart().isZero 7 | val end = relativeEnd().samePoint(1.0, 1.0, 1.0) 8 | start && end 9 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/CircularList.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | class CircularList(private val list: List) : List by list { 4 | override fun get(index: Int): T { 5 | if (isEmpty()) { 6 | throw NoSuchElementException("List is empty.") 7 | } else { 8 | return list[index % size] 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/CoordinateUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | import net.minestom.server.coordinate.BlockVec 4 | import net.minestom.server.coordinate.Point 5 | import net.minestom.server.coordinate.Pos 6 | import net.minestom.server.coordinate.Vec 7 | import kotlin.math.abs 8 | import kotlin.math.min 9 | 10 | operator fun Point.component1() = x() 11 | operator fun Point.component2() = y() 12 | operator fun Point.component3() = z() 13 | 14 | operator fun Pos.component4() = yaw 15 | operator fun Pos.component5() = pitch 16 | 17 | fun Point.toVec(): Vec = Vec.fromPoint(this) 18 | fun Point.toBlockVec(): BlockVec = this as? BlockVec ?: BlockVec(x(), y(), z()) 19 | 20 | fun Pos.round() = Pos( 21 | (if (x < 0) (x - 0.5).toInt() else x.toInt()).toDouble(), 22 | y.toInt().toDouble(), 23 | (if (z < 0) (z - 0.5).toInt() else z.toInt()).toDouble(), 24 | ) 25 | 26 | object CoordinateUtils { 27 | fun getAllInBox(pos1: Pos, pos2: Pos): List { 28 | val dx = abs(pos2.blockX() - pos1.blockX()) 29 | val dy = abs(pos2.blockY() - pos1.blockY()) 30 | val dz = abs(pos2.blockZ() - pos1.blockZ()) 31 | val minX = min(pos1.blockX(), pos2.blockX()) 32 | val minY = min(pos1.blockY(), pos2.blockY()) 33 | val minZ = min(pos1.blockZ(), pos2.blockZ()) 34 | return (0 .. dx).flatMap { x -> 35 | (0 .. dy).flatMap { y -> 36 | (0 .. dz).map { z -> 37 | Pos(x.toDouble() + minX, y.toDouble() + minY, z.toDouble() + minZ) 38 | } 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/EventUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | import kotlinx.coroutines.* 4 | import net.minestom.server.event.Event 5 | import net.minestom.server.event.EventNode 6 | import java.util.function.Consumer 7 | import kotlin.coroutines.CoroutineContext 8 | 9 | /** 10 | * Shortcut for [EventNode.addListener] using a reified type parameter. 11 | */ 12 | inline fun EventNode.listen(consumer: Consumer) { 13 | this.addListener(T::class.java, consumer) 14 | } 15 | 16 | /** 17 | * Listens to an event and runs a suspending handler in a 18 | * blocking context. This will prevent other handlers from 19 | * executing, so this should only be used when necessary. 20 | */ 21 | inline fun EventNode.listenSuspend(crossinline consumer: suspend (T) -> Unit) { 22 | this.addListener(T::class.java) { event -> 23 | runBlocking { 24 | consumer.invoke(event) 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Listens to an event and runs a handler in a coroutine. 31 | * Modifications and cancellations to the event will do nothing. 32 | */ 33 | inline fun EventNode.listenAsync(crossinline consumer: suspend (T) -> Unit) { 34 | this.addListener(T::class.java) { event -> 35 | asyncEventCoroutineScope.launch { 36 | consumer.invoke(event) 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * A [CoroutineScope] which should only be used 43 | * for executing 'async' event handlers. 44 | */ 45 | val asyncEventCoroutineScope = object : CoroutineScope { 46 | override val coroutineContext: CoroutineContext = 47 | Dispatchers.IO + SupervisorJob() + CoroutineName("Async Event Handling") 48 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/FireworkUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | import net.kyori.adventure.sound.Sound 4 | import net.minestom.server.MinecraftServer 5 | import net.minestom.server.component.DataComponents 6 | import net.minestom.server.coordinate.Pos 7 | import net.minestom.server.entity.Entity 8 | import net.minestom.server.entity.EntityType 9 | import net.minestom.server.entity.metadata.projectile.FireworkRocketMeta 10 | import net.minestom.server.instance.Instance 11 | import net.minestom.server.item.ItemStack 12 | import net.minestom.server.item.Material 13 | import net.minestom.server.item.component.FireworkList 14 | import net.minestom.server.network.packet.server.play.EntityStatusPacket 15 | import net.minestom.server.sound.SoundEvent 16 | import java.time.Duration 17 | 18 | object FireworkUtils { 19 | 20 | fun spawnFirework(instance: Instance, position: Pos, millisBeforeDetonate: Long, fireworkMeta: FireworkList) { 21 | val fireworkItem = ItemStack.builder(Material.FIREWORK_ROCKET).set(DataComponents.FIREWORKS, fireworkMeta).build() 22 | val firework = Entity(EntityType.FIREWORK_ROCKET) 23 | (firework.entityMeta as FireworkRocketMeta).fireworkInfo = fireworkItem 24 | firework.setNoGravity(true) 25 | firework.setInstance(instance, position) 26 | instance.playSound( 27 | Sound.sound( 28 | SoundEvent.ENTITY_FIREWORK_ROCKET_LAUNCH, Sound.Source.PLAYER, 2.0f, 1.0f 29 | ), position 30 | ) 31 | MinecraftServer.getSchedulerManager().buildTask { 32 | instance.sendGroupedPacket(EntityStatusPacket(firework.entityId, 17)) 33 | firework.remove() 34 | }.delay(Duration.ofMillis(millisBeforeDetonate)).schedule() 35 | } 36 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/GameState.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | import com.bluedragonmc.api.grpc.CommonTypes.EnumGameState 4 | 5 | enum class GameState(val canPlayersJoin: Boolean) { 6 | SERVER_STARTING(false), 7 | WAITING(true), 8 | STARTING(true), 9 | INGAME(false), 10 | ENDING(false); 11 | 12 | fun mapToRpcState() = when (this) { 13 | SERVER_STARTING -> EnumGameState.INITIALIZING 14 | WAITING -> EnumGameState.WAITING 15 | STARTING -> EnumGameState.STARTING 16 | INGAME -> EnumGameState.INGAME 17 | ENDING -> EnumGameState.ENDING 18 | } 19 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/InstanceUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | import com.bluedragonmc.api.grpc.CommonTypes.GameType.GameTypeFieldSelector 4 | import com.bluedragonmc.api.grpc.gameType 5 | import com.bluedragonmc.server.Game 6 | import com.bluedragonmc.server.api.Environment 7 | import net.kyori.adventure.text.Component 8 | import net.minestom.server.MinecraftServer 9 | import net.minestom.server.event.EventNode 10 | import net.minestom.server.event.trait.PlayerEvent 11 | import net.minestom.server.instance.Instance 12 | import net.minestom.server.timer.Task 13 | import java.time.Duration 14 | import java.util.* 15 | import java.util.concurrent.CompletableFuture 16 | 17 | object InstanceUtils { 18 | 19 | /** 20 | * Forcefully removes all players from the instance 21 | * and unregisters it, sending all players to a lobby. 22 | * @return a CompletableFuture when all players are removed and the instance is unregistered. 23 | */ 24 | fun forceUnregisterInstance(instance: Instance): CompletableFuture { 25 | val eventNode = EventNode.all("temp-vacate-${UUID.randomUUID()}") 26 | eventNode.addListener(PlayerEvent::class.java) { event -> 27 | if (event.player.instance == instance) { 28 | event.player.kick(Component.text("This instance is shutting down.")) 29 | event.player.remove() 30 | } 31 | } 32 | MinecraftServer.getGlobalEventHandler().addChild(eventNode) 33 | return vacateInstance(instance).thenRun { 34 | MinecraftServer.getSchedulerManager().scheduleNextTick { 35 | MinecraftServer.getInstanceManager().unregisterInstance(instance) 36 | MinecraftServer.getGlobalEventHandler().removeChild(eventNode) 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * Send all players in the instance to a lobby. 43 | * @return a [CompletableFuture] which is completed when all players are removed from the instance. 44 | */ 45 | fun vacateInstance(instance: Instance): CompletableFuture { 46 | if (instance.players.isEmpty()) { 47 | // If the instance is already empty, return immediately 48 | return CompletableFuture.completedFuture(null) 49 | } else { 50 | // If the instance is not empty, attempt to send all players to a lobby 51 | val lobby = Game.games.find { it.name == Environment.defaultGameName } 52 | if (lobby != null) { 53 | return CompletableFuture.allOf( 54 | *instance.players.map { 55 | lobby.addPlayer(it, sendPlayer = true) 56 | }.toTypedArray() 57 | ) 58 | } else { 59 | // If there's no lobby, repeatedly queue players for a lobby on another server 60 | // until every player has disconnected 61 | val future = CompletableFuture() 62 | var completionTask: Task? = null 63 | val queueTask: Task = MinecraftServer.getSchedulerManager().buildTask { 64 | instance.players.forEach { 65 | Environment.queue.queue(it, gameType { 66 | name = Environment.defaultGameName 67 | selectors += GameTypeFieldSelector.GAME_NAME 68 | }) 69 | } 70 | }.repeat(Duration.ofSeconds(10)).schedule() 71 | completionTask = MinecraftServer.getSchedulerManager().buildTask { 72 | if (instance.players.isEmpty()) { 73 | future.complete(null) 74 | completionTask?.cancel() 75 | queueTask.cancel() 76 | } 77 | }.repeat(Duration.ofSeconds(1)).schedule() 78 | return future 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/ItemUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.kyori.adventure.text.format.NamedTextColor 5 | import net.minestom.server.color.Color 6 | import net.minestom.server.component.DataComponents 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.item.ItemStack 9 | import net.minestom.server.item.Material 10 | import net.minestom.server.item.component.EnchantmentList 11 | import net.minestom.server.item.enchant.Enchantment 12 | 13 | object ItemUtils { 14 | fun knockbackStick(kbLevel: Int, player: Player): ItemStack = 15 | ItemStack.builder(Material.STICK).set(DataComponents.ITEM_NAME, Component.translatable("global.items.kb_stick.name")) 16 | .set(DataComponents.LORE, splitAndFormatLore(Component.translatable("global.items.kb_stick.lore"), NamedTextColor.GRAY, player)) 17 | .set(DataComponents.ENCHANTMENTS, EnchantmentList(Enchantment.KNOCKBACK, kbLevel)) 18 | .build() 19 | 20 | fun ItemStack.withArmorColor(color: Color): ItemStack { 21 | return with(DataComponents.DYED_COLOR, color) 22 | } 23 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | import java.time.Duration 4 | 5 | fun formatDuration(millis: Long, milliseconds: Boolean = true): String = 6 | formatDuration(Duration.ofMillis(millis), milliseconds) 7 | 8 | fun formatDuration(duration: Duration, milliseconds: Boolean = true): String { 9 | return if (milliseconds) { 10 | String.format("%02d:%02d:%02d.%03d", 11 | duration.toHoursPart(), 12 | duration.toMinutesPart(), 13 | duration.toSecondsPart(), 14 | duration.toMillisPart()) 15 | } else { 16 | String.format("%02d:%02d:%02d", 17 | duration.toHoursPart(), 18 | duration.toMinutesPart(), 19 | duration.toSecondsPart()) 20 | } 21 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/TaskUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils 2 | 3 | import com.bluedragonmc.server.Game 4 | import com.bluedragonmc.server.event.GameEvent 5 | import com.bluedragonmc.server.event.GameStateChangedEvent 6 | import com.bluedragonmc.server.module.GameModule 7 | import com.bluedragonmc.server.module.minigame.WinModule 8 | import net.minestom.server.event.Event 9 | import net.minestom.server.event.EventNode 10 | import net.minestom.server.timer.Task 11 | import java.util.function.Predicate 12 | 13 | /** 14 | * Cancels the task when any event of the given [eventType] is triggered in the [game]. 15 | * The received event must pass the [condition] to cancel the task. 16 | * @return The task, for method chaining 17 | */ 18 | fun Task.cancelOn(game: Game, eventType: Class, condition: Predicate = Predicate { true }): Task { 19 | lateinit var module: GameModule 20 | module = object : GameModule() { 21 | override fun initialize(parent: Game, eventNode: EventNode) { 22 | eventNode.addListener(eventType) { event -> 23 | if (condition.test(event)) { 24 | logger.debug("Canceling task with id ${this@cancelOn.id()} because event of type ${eventType.simpleName} was triggered.") 25 | game.unregister(module) // `unregister` triggers `deinitialize`, which cancels the task 26 | } 27 | } 28 | } 29 | 30 | override fun deinitialize() { 31 | this@cancelOn.cancel() 32 | } 33 | } 34 | game.register(module) { true } 35 | game.modules.add(module) 36 | return this 37 | } 38 | 39 | /** 40 | * Cancels the task when the game state is set to ENDING, a winner is declared, or game modules are uninitialized, whichever comes first. 41 | * @return The task, for method chaining 42 | */ 43 | fun Task.manage(game: Game): Task = cancelOn(game, GameEvent::class.java) { event -> 44 | when (event) { 45 | is GameStateChangedEvent -> event.newState == GameState.ENDING 46 | is WinModule.WinnerDeclaredEvent -> true 47 | else -> !Game.games.contains(game) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /common/src/main/kotlin/com/bluedragonmc/server/utils/packet/GlowingEntityUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.utils.packet 2 | 3 | import net.kyori.adventure.text.format.NamedTextColor 4 | import net.minestom.server.MinecraftServer 5 | import net.minestom.server.adventure.audience.PacketGroupingAudience 6 | import net.minestom.server.entity.Entity 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.network.packet.server.play.TeamsPacket 9 | import net.minestom.server.scoreboard.Team 10 | import net.minestom.server.tag.Tag 11 | import java.util.* 12 | 13 | object GlowingEntityUtils { 14 | 15 | private val SEEN_TEAMS_TAG = Tag.String("seen_glow_teams").list() 16 | private val CURRENT_GLOW_TAG = Tag.String("current_glow_team") 17 | private val teamCache = mutableMapOf() 18 | 19 | fun glow(entity: Entity, color: NamedTextColor, viewers: Collection) { 20 | entity.isGlowing = true 21 | val team = teamCache.getOrPut(color) { 22 | MinecraftServer.getTeamManager().createBuilder(UUID.randomUUID().toString()).teamColor(color).build() 23 | } 24 | 25 | // Send the team creation packet to all viewers that have not been sent this packet before. 26 | PacketGroupingAudience.of(viewers.filter { 27 | !it.hasTag(SEEN_TEAMS_TAG) || !it.getTag(SEEN_TEAMS_TAG)!!.contains(team.teamName) 28 | }).sendGroupedPacket(team.createTeamsCreationPacket()) 29 | 30 | PacketGroupingAudience.of(viewers).sendGroupedPacket( 31 | TeamsPacket(team.teamName, TeamsPacket.AddEntitiesToTeamAction(listOf(entity.uuid.toString()))) 32 | ) 33 | viewers.forEach { viewer -> 34 | if (!viewer.hasTag(SEEN_TEAMS_TAG)) { 35 | viewer.setTag(SEEN_TEAMS_TAG, listOf(team.teamName)) 36 | } else { 37 | viewer.setTag(SEEN_TEAMS_TAG, viewer.getTag(SEEN_TEAMS_TAG) + team.teamName) 38 | } 39 | } 40 | entity.setTag(CURRENT_GLOW_TAG, team.teamName) 41 | } 42 | 43 | fun removeGlow(entity: Entity, viewers: Collection) { 44 | val team = entity.getTag(CURRENT_GLOW_TAG) 45 | PacketGroupingAudience.of(viewers).sendGroupedPacket(TeamsPacket( 46 | team, TeamsPacket.RemoveEntitiesToTeamAction(listOf(entity.uuid.toString())) 47 | )) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Run using `docker compose up` (Compose V2 is integrated into docker and doesn't require a hyphen) 2 | version: "3.9" 3 | services: 4 | server: 5 | build: . 6 | ports: 7 | - "0.0.0.0:25565:25565" 8 | volumes: 9 | - ./worlds:/server/worlds 10 | networks: 11 | - puffin_network 12 | mongo: 13 | image: mongo 14 | ports: 15 | - "27017:27017" 16 | hostname: mongo 17 | networks: 18 | - puffin_network 19 | rabbitmq: 20 | image: rabbitmq 21 | ports: 22 | - "15692:15692" # monitoring interface 23 | - "5672:5672" # AQMP port 24 | hostname: rabbitmq 25 | networks: 26 | - puffin_network 27 | networks: 28 | puffin_network: 29 | external: true -------------------------------------------------------------------------------- /favicon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueDragonMC/Server/ed5d60bf9eb85cfa653bd240c4216efdfb423fa1/favicon_64.png -------------------------------------------------------------------------------- /favicon_hq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueDragonMC/Server/ed5d60bf9eb85cfa653bd240c4216efdfb423fa1/favicon_hq.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.caching=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | metadata.format.version = "1.1" 2 | 3 | [versions] 4 | kotlin = "2.1.10" 5 | minestom = "1_21_5-c4814c2270" 6 | configurate = "4.2.0" 7 | minimessage = "4.19.0" 8 | kmongo = "5.2.1" 9 | caffeine = "3.2.0" 10 | okhttp = "4.12.0" 11 | serialization = "1.8.0" 12 | tinylog = "2.7.0" 13 | atlas-projectiles = "2.1.2" 14 | # Auto-generated GRPC/Protobuf messaging code 15 | rpc = "bff6cb5ac2" 16 | # Messaging dependencies 17 | grpc = "1.71.0" 18 | grpc-kotlin-stub = "1.4.1" 19 | protobuf-kotlin = "4.30.1" 20 | # Testing dependencies 21 | mockk = "1.13.11" 22 | junit = "5.9.0" 23 | 24 | [libraries] 25 | kotlin-jvm = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } 26 | minestom = { group = "net.minestom", name = "minestom-snapshots", version.ref = "minestom" } 27 | configurate = { group = "org.spongepowered", name = "configurate-yaml", version.ref = "configurate" } 28 | configurate-extra-kotlin = { group = "org.spongepowered", name = "configurate-extra-kotlin", version.ref = "configurate" } 29 | minimessage = { group = "net.kyori", name = "adventure-text-minimessage", version.ref = "minimessage" } 30 | atlas-projectiles = { group = "ca.atlasengine", name = "atlas-projectiles", version.ref = "atlas-projectiles" } 31 | kmongo = { group = "org.litote.kmongo", name = "kmongo-coroutine-serialization", version.ref = "kmongo" } 32 | serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } 33 | caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version.ref = "caffeine" } 34 | okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } 35 | rpc = { group = "com.github.BlueDragonMC", name = "RPC", version.ref = "rpc" } 36 | #rpc = { group = "com.bluedragonmc", name = "rpc", version = "1.0" } 37 | grpc-protobuf = { group = "io.grpc", name = "grpc-protobuf", version.ref = "grpc" } 38 | grpc-netty = { group = "io.grpc", name = "grpc-netty", version.ref = "grpc" } 39 | grpc-kotlin-stub = { group = "io.grpc", name = "grpc-kotlin-stub", version.ref = "grpc-kotlin-stub" } 40 | #grpc-services = { group = "io.grpc", name = "grpc-services", version.ref = "grpc" } 41 | protobuf-kotlin = { group = "com.google.protobuf", name = "protobuf-kotlin", version.ref = "protobuf-kotlin" } 42 | mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } 43 | junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } 44 | junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } 45 | tinylog-api = { group = "org.tinylog", name = "tinylog-api", version.ref = "tinylog"} 46 | tinylog-impl = { group = "org.tinylog", name = "tinylog-impl", version.ref = "tinylog"} 47 | tinylog-slf4j = { group = "org.tinylog", name = "slf4j-tinylog", version.ref = "tinylog"} 48 | 49 | [bundles] 50 | configurate = ["configurate", "configurate-extra-kotlin"] 51 | messaging = ["rpc", "grpc-protobuf", "grpc-netty", "grpc-kotlin-stub", "protobuf-kotlin"] 52 | tinylog = ["tinylog-api", "tinylog-impl", "tinylog-slf4j"] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueDragonMC/Server/ed5d60bf9eb85cfa653bd240c4216efdfb423fa1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk21 -------------------------------------------------------------------------------- /production.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1 2 | # This Dockerfile runs on the CI/CD pipeline when the Server is being deployed. 3 | 4 | # Build the project into an executable JAR 5 | FROM docker.io/library/gradle:8.13-jdk21 as build 6 | # Copy build files and source code 7 | COPY . /work 8 | WORKDIR /work 9 | # Run gradle in the /work directory 10 | RUN /usr/bin/gradle --console=plain --info --stacktrace --no-daemon build 11 | 12 | # Run the built JAR and expose port 25565 13 | FROM docker.io/library/eclipse-temurin:21-jre-alpine 14 | EXPOSE 25565 15 | EXPOSE 50051 16 | WORKDIR /server 17 | 18 | LABEL com.bluedragonmc.image=server 19 | LABEL com.bluedragonmc.environment=production 20 | 21 | COPY favicon_64.png /server/favicon_64.png 22 | 23 | # Copy the built JAR from the previous step 24 | COPY --from=build /work/build/libs/Server-*-all.jar /server/server.jar 25 | 26 | # Run the server 27 | CMD ["java", "-jar", "/server/server.jar"] -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Server" 2 | 3 | pluginManagement { 4 | includeBuild("build-logic") 5 | } 6 | 7 | include("common") 8 | include("testing") 9 | -------------------------------------------------------------------------------- /src/main/kotlin-templates/com/bluedragonmc/server/GitVersionInfo.kt.peb: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server 2 | 3 | /** 4 | * This file has placeholders which are automatically filled 5 | * by the Blossom Gradle plugin. See the root project's 6 | * build.gradle.kts for more context. 7 | */ 8 | object GitVersionInfo : VersionInfo { 9 | 10 | override val COMMIT: String? = "{{ gitCommit | default("Unknown") }}" 11 | override val BRANCH: String? = "{{ gitBranch | default("Unknown") }}" 12 | override val COMMIT_DATE: String? = "{{ gitCommitDate | default("Unknown") }}" 13 | 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/Server.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server 2 | 3 | import com.bluedragonmc.server.api.Environment 4 | import com.bluedragonmc.server.bootstrap.* 5 | import com.bluedragonmc.server.bootstrap.dev.DevInstanceRouter 6 | import com.bluedragonmc.server.bootstrap.dev.MojangAuthentication 7 | import com.bluedragonmc.server.bootstrap.dev.OpenToLAN 8 | import com.bluedragonmc.server.bootstrap.prod.AgonesIntegration 9 | import com.bluedragonmc.server.bootstrap.prod.InitialInstanceRouter 10 | import com.bluedragonmc.server.bootstrap.prod.VelocityForwarding 11 | import com.bluedragonmc.server.queue.GameLoader 12 | import com.bluedragonmc.server.queue.createEnvironment 13 | import net.minestom.server.MinecraftServer 14 | import org.slf4j.LoggerFactory 15 | import java.text.DateFormat 16 | import kotlin.system.exitProcess 17 | import kotlin.system.measureTimeMillis 18 | 19 | lateinit var lobby: Game 20 | fun isLobbyInitialized() = ::lobby.isInitialized 21 | private val logger = LoggerFactory.getLogger("ServerKt") 22 | 23 | fun main() { 24 | val commitDate = GitVersionInfo.commitDate 25 | if (commitDate != null) { 26 | val str = DateFormat.getDateInstance().format(commitDate) 27 | logger.info("Starting server version ${GitVersionInfo.BRANCH}/${GitVersionInfo.COMMIT} ($str)") 28 | } else { 29 | logger.info("Starting server version ${GitVersionInfo.BRANCH}/${GitVersionInfo.COMMIT}") 30 | } 31 | 32 | val time = measureTimeMillis(::start) 33 | logger.info("Game server started in ${time}ms.") 34 | } 35 | 36 | fun start() { 37 | 38 | Environment.setEnvironment(createEnvironment()) 39 | logger.info("Starting Minecraft server in environment ${Environment.current::class.simpleName}") 40 | 41 | val minecraftServer = MinecraftServer.init() 42 | val eventNode = MinecraftServer.getGlobalEventHandler() 43 | 44 | val services = listOf( 45 | AgonesIntegration, 46 | Commands, 47 | CustomPlayerProvider, 48 | DefaultDimensionTypes, 49 | DevInstanceRouter, 50 | GlobalBlockHandlers, 51 | GlobalChatFormat, 52 | GlobalPlayerNameFormat, 53 | GlobalPunishments, 54 | GlobalTranslation, 55 | InitialInstanceRouter, 56 | IntegrationsInit, 57 | MojangAuthentication, 58 | OpenToLAN, 59 | PerInstanceChat, 60 | PerInstanceTabList, 61 | ServerListPingHandler, 62 | TabListFormat, 63 | VelocityForwarding 64 | ).filter { it.canHook() } 65 | 66 | // Load game plugins and preinitialize their main classes 67 | GameLoader.loadGames() 68 | 69 | services.forEach { 70 | logger.debug("Initializing service ${it::class.simpleName ?: it::class.qualifiedName}") 71 | it.hook(eventNode) 72 | } 73 | 74 | logger.info("Initialized ${services.size} services in environment ${Environment.current::class.simpleName}.") 75 | 76 | // Start the queue, allowing players to queue for and join games 77 | Environment.queue.start() 78 | 79 | // Start the server & bind to port 25565 80 | minecraftServer.start("0.0.0.0", 25565) 81 | 82 | // Create a Lobby instance 83 | lobby = try { 84 | GameLoader.createNewGame(Environment.defaultGameName, null, null) 85 | } catch (e: Throwable) { 86 | logger.error("There was an error initializing the Lobby. Shutting down...") 87 | e.printStackTrace() 88 | MinecraftServer.stopCleanly() 89 | exitProcess(1) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/Bootstrap.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.api.Environment 4 | import net.minestom.server.event.Event 5 | import net.minestom.server.event.EventNode 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory 8 | 9 | abstract class Bootstrap(private val envType: EnvType = EnvType.ANY) { 10 | 11 | enum class EnvType { 12 | PRODUCTION, DEVELOPMENT, ANY 13 | } 14 | 15 | protected val logger: Logger by lazy { 16 | LoggerFactory.getLogger(this::class.java) 17 | } 18 | 19 | abstract fun hook(eventNode: EventNode) 20 | 21 | fun canHook(): Boolean { 22 | return envType == EnvType.ANY || if (Environment.current.isDev) { 23 | envType == EnvType.DEVELOPMENT 24 | } else { 25 | envType == EnvType.PRODUCTION 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/Commands.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.command.* 4 | import com.bluedragonmc.server.command.punishment.* 5 | import net.kyori.adventure.text.Component 6 | import net.kyori.adventure.text.format.NamedTextColor 7 | import net.minestom.server.MinecraftServer 8 | import net.minestom.server.event.Event 9 | import net.minestom.server.event.EventNode 10 | 11 | object Commands : Bootstrap() { 12 | override fun hook(eventNode: EventNode) { 13 | listOf( 14 | FlyCommand("fly"), 15 | GameCommand("game", "/game "), 16 | GameModeCommand("gamemode", "/gamemode [player]", "gm"), 17 | GameModeCommand.GameModeAdventureCommand(), 18 | GameModeCommand.GameModeCreativeCommand(), 19 | GameModeCommand.GameModeSpectatorCommand(), 20 | GameModeCommand.GameModeSurvivalCommand(), 21 | GiveCommand("give", "/give [player] "), 22 | InstanceCommand("instance", "/instance ...", "in"), 23 | JoinCommand("join", "/join [mode] [mapName]"), 24 | KickCommand("kick", "/kick "), 25 | KillCommand("kill", "/kill [player]"), 26 | LeaderboardCommand("leaderboard", "/leaderboard "), 27 | ListCommand("list"), 28 | LobbyCommand("lobby", "/lobby", "l", "hub"), 29 | MessageCommand("msg", "message", "w", "tell"), 30 | MindecraftesCommand("mindecraftes", "/mindecraftes"), 31 | PardonCommand("pardon", "/pardon ", "unban", "unmute"), 32 | PartyCommand("party", "/party ...", "p"), 33 | PartyChatShorthandCommand("pchat", "/pc ", "pc", "partychat"), 34 | PingCommand("ping", "/ping", "latency"), 35 | PlaysoundCommand("playsound", "/playsound [position] [volume] [pitch]", "ps"), 36 | PunishCommand("ban", "/ ", "mute"), 37 | SetBlockCommand("setblock", "/setblock "), 38 | StopCommand("stop", "/stop"), 39 | TeleportCommand("tp", "/tp > [player| ]"), 40 | TimeCommand("time", "/time ..."), 41 | VersionCommand("version", "/version", "icanhasminestom", "ver", "pl"), 42 | ViewPunishmentCommand("punishment", "/punishment ", "vp"), 43 | ViewPunishmentsCommand("punishments", "/punishments ", "vps", "history"), 44 | ).forEach(MinecraftServer.getCommandManager()::register) 45 | 46 | MinecraftServer.getCommandManager().setUnknownCommandCallback { sender, command -> 47 | sender.sendMessage(Component.translatable("commands.help.failed", NamedTextColor.RED)) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/CustomPlayerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.CustomPlayer 4 | import net.minestom.server.MinecraftServer 5 | import net.minestom.server.event.Event 6 | import net.minestom.server.event.EventNode 7 | 8 | object CustomPlayerProvider : Bootstrap() { 9 | override fun hook(eventNode: EventNode) { 10 | // Set a custom player provider, so we can easily add fields to the Player class 11 | MinecraftServer.getConnectionManager().setPlayerProvider(::CustomPlayer) 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/DefaultDimensionTypes.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.module.instance.CustomGeneratorInstanceModule 4 | import net.minestom.server.event.Event 5 | import net.minestom.server.event.EventNode 6 | 7 | object DefaultDimensionTypes : Bootstrap() { 8 | override fun hook(eventNode: EventNode) { 9 | // Register the fullbright dimension before the server starts, 10 | // fixing an issue where clients that haven't received the dimension 11 | // type get kicked when trying to switch instances: https://github.com/Minestom/Minestom/issues/2229 12 | CustomGeneratorInstanceModule.getFullbrightDimension() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/GlobalBlockHandlers.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import net.kyori.adventure.key.Key 4 | import net.minestom.server.MinecraftServer 5 | import net.minestom.server.event.Event 6 | import net.minestom.server.event.EventNode 7 | import net.minestom.server.instance.block.BlockHandler 8 | import net.minestom.server.tag.Tag 9 | 10 | object GlobalBlockHandlers : Bootstrap() { 11 | override fun hook(eventNode: EventNode) { 12 | registerHandler( 13 | "minecraft:sign", listOf( 14 | // https://minecraft.wiki/w/Sign#Block_data 15 | Tag.Byte("is_waxed"), 16 | Tag.NBT("front_text"), 17 | Tag.NBT("back_text"), 18 | ) 19 | ) 20 | registerHandler( 21 | "minecraft:skull", listOf( 22 | Tag.String("ExtraType"), 23 | Tag.NBT("SkullOwner") 24 | ) 25 | ) 26 | registerHandler( 27 | "minecraft:beacon", listOf( 28 | Tag.Component("CustomName"), 29 | Tag.String("Lock"), 30 | Tag.Integer("Levels"), 31 | Tag.Integer("Primary"), 32 | Tag.Integer("Secondary") 33 | ) 34 | ) 35 | registerHandler( 36 | "minecraft:furnace", listOf( 37 | Tag.Short("BurnTime"), 38 | Tag.Short("CookTime"), 39 | Tag.Short("CookTimeTotal"), 40 | Tag.Component("CustomName"), 41 | Tag.ItemStack("Items").list(), 42 | Tag.String("Lock"), 43 | Tag.Integer("RecipesUsed").list() 44 | ) 45 | ) 46 | registerHandler( 47 | "minecraft:banner", listOf( 48 | Tag.String("CustomName"), 49 | Tag.NBT("Patterns").list() 50 | ) 51 | ) 52 | registerHandler( 53 | "minecraft:dropper", listOf( 54 | Tag.String("CustomName"), 55 | Tag.ItemStack("Items").list(), 56 | Tag.String("Lock"), 57 | Tag.String("LootTable"), 58 | Tag.Long("LootTableSeed") 59 | ) 60 | ) 61 | registerHandler("minecraft:daylight_detector", listOf()) 62 | registerHandler( 63 | "minecraft:chest", listOf( 64 | Tag.String("CustomName"), 65 | Tag.ItemStack("Items").list(), 66 | Tag.String("Lock"), 67 | Tag.String("LootTable"), 68 | Tag.Long("LootTableSeed") 69 | ) 70 | ) 71 | } 72 | 73 | private fun registerHandler(registryName: String, blockEntityTags: List>) = 74 | MinecraftServer.getBlockManager().registerHandler(registryName) { 75 | createHandler(registryName, blockEntityTags) 76 | } 77 | 78 | private fun createHandler(registryName: String, blockEntityTags: List>) = object : BlockHandler { 79 | override fun getKey() = Key.key(registryName) 80 | override fun getBlockEntityTags() = blockEntityTags 81 | } 82 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/GlobalChatFormat.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.ALT_COLOR_1 4 | import com.bluedragonmc.server.ALT_COLOR_2 5 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_1 6 | import com.bluedragonmc.server.CustomPlayer 7 | import com.bluedragonmc.server.service.Permissions 8 | import com.bluedragonmc.server.utils.buildComponent 9 | import com.bluedragonmc.server.utils.miniMessage 10 | import com.bluedragonmc.server.utils.surroundWithSeparators 11 | import net.kyori.adventure.text.Component 12 | import net.kyori.adventure.text.event.HoverEvent 13 | import net.kyori.adventure.text.format.NamedTextColor 14 | import net.minestom.server.event.Event 15 | import net.minestom.server.event.EventFilter 16 | import net.minestom.server.event.EventNode 17 | import net.minestom.server.event.player.PlayerChatEvent 18 | 19 | object GlobalChatFormat : Bootstrap() { 20 | override fun hook(eventNode: EventNode) { 21 | val child = EventNode.event("global-chat-format", EventFilter.ALL) { true } 22 | child.priority = Integer.MAX_VALUE // High priority; runs last 23 | eventNode.addChild(child) 24 | child.addListener(PlayerChatEvent::class.java) { event -> 25 | val player = event.player as CustomPlayer 26 | player.getFirstMute()?.let { mute -> 27 | event.isCancelled = true 28 | event.player.sendMessage(GlobalPunishments.getMuteMessage(mute).surroundWithSeparators()) 29 | return@addListener 30 | } 31 | val experience = (player).run { if (isDataInitialized()) data.experience else 0 } 32 | val level = CustomPlayer.getXpLevel(experience) 33 | val xpToNextLevel = CustomPlayer.getXpToNextLevel(level, experience) 34 | 35 | val prefix = Permissions.getMetadata(player.uuid).prefix 36 | 37 | event.isCancelled = true 38 | val component = buildComponent { 39 | +buildComponent { 40 | +Component.text("[", NamedTextColor.DARK_GRAY) 41 | +Component.text(level.toInt(), BRAND_COLOR_PRIMARY_1) 42 | +Component.text("] ", NamedTextColor.DARK_GRAY) 43 | }.hoverEvent(HoverEvent.showText(Component.translatable("global.chat_xp_hover", 44 | NamedTextColor.GRAY, 45 | event.player.name, 46 | Component.text(experience, NamedTextColor.GREEN), 47 | Component.text(xpToNextLevel, ALT_COLOR_1), 48 | Component.text(level.toInt() + 1, ALT_COLOR_2)))) 49 | 50 | +prefix 51 | +player.name 52 | +Component.text(": ", NamedTextColor.DARK_GRAY) 53 | if (Permissions.hasPermission(player.uuid, "chat.minimessage") == true) 54 | +miniMessage.deserialize(event.rawMessage) 55 | else +Component.text(event.rawMessage, NamedTextColor.WHITE) 56 | } 57 | 58 | // Currently, setting the chat format does not allow for translation with GlobalTranslator 59 | // because it always sends a GroupedPacket. Instead, we cancel the event and send the messages individually. 60 | event.recipients.forEach { it.sendMessage(component) } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/GlobalPlayerNameFormat.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.event.DataLoadedEvent 4 | import com.bluedragonmc.server.service.Permissions 5 | import com.bluedragonmc.server.utils.withColor 6 | import net.minestom.server.event.Event 7 | import net.minestom.server.event.EventNode 8 | 9 | object GlobalPlayerNameFormat : Bootstrap() { 10 | override fun hook(eventNode: EventNode) { 11 | eventNode.addListener(DataLoadedEvent::class.java) { event -> 12 | event.player.displayName = event.player.name.withColor( 13 | Permissions.getMetadata(event.player.uuid).rankColor 14 | ) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/GlobalPunishments.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.CustomPlayer 4 | import com.bluedragonmc.server.event.DataLoadedEvent 5 | import com.bluedragonmc.server.model.Punishment 6 | import com.bluedragonmc.server.utils.buildComponent 7 | import net.kyori.adventure.text.Component 8 | import net.kyori.adventure.text.format.NamedTextColor 9 | import net.kyori.adventure.text.format.TextDecoration 10 | import net.minestom.server.event.Event 11 | import net.minestom.server.event.EventNode 12 | 13 | object GlobalPunishments : Bootstrap() { 14 | override fun hook(eventNode: EventNode) { 15 | eventNode.addListener(DataLoadedEvent::class.java) { event -> 16 | val player = event.player as CustomPlayer 17 | val ban = player.getFirstBan() 18 | if (ban != null) { 19 | player.kick(getBanMessage(ban)) 20 | } 21 | } 22 | } 23 | 24 | private fun getPunishmentMessage(titleKey: String, punishment: Punishment) = buildComponent { 25 | +Component.translatable(titleKey, NamedTextColor.RED, TextDecoration.UNDERLINED) 26 | +Component.newline() 27 | +Component.newline() 28 | +Component.translatable("punishment.field.reason", Component.text(punishment.reason, NamedTextColor.WHITE)) 29 | +Component.newline() 30 | +Component.translatable("punishment.field.expiry", Component.text(punishment.getTimeRemaining(), NamedTextColor.WHITE)) 31 | +Component.newline() 32 | +Component.translatable("punishment.field.id", Component.text(punishment.id.toString().substringBefore('-'), NamedTextColor.WHITE)) 33 | // Kick messages with a non-breaking space (U+00A0) will prevent the player from being immediately connected to another server 34 | // This is a way of differentiating intentional vs. accidental kicks that remains invisible to the end user 35 | +Component.text("\u00A0") 36 | } 37 | 38 | fun getBanMessage(punishment: Punishment) = getPunishmentMessage("punishment.ban.title", punishment) 39 | fun getMuteMessage(punishment: Punishment) = getPunishmentMessage("punishment.mute.title", punishment) 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/IntegrationsInit.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.api.Environment 4 | import com.bluedragonmc.server.api.IncomingRPCHandlerStub 5 | import com.bluedragonmc.server.api.OutgoingRPCHandlerStub 6 | import com.bluedragonmc.server.impl.DatabaseConnectionImpl 7 | import com.bluedragonmc.server.impl.IncomingRPCHandlerImpl 8 | import com.bluedragonmc.server.impl.OutgoingRPCHandlerImpl 9 | import com.bluedragonmc.server.impl.PermissionManagerImpl 10 | import com.bluedragonmc.server.service.Database 11 | import com.bluedragonmc.server.service.Messaging 12 | import com.bluedragonmc.server.service.Permissions 13 | import kotlinx.coroutines.runBlocking 14 | import net.minestom.server.event.Event 15 | import net.minestom.server.event.EventNode 16 | import java.net.InetAddress 17 | 18 | object IntegrationsInit : Bootstrap() { 19 | override fun hook(eventNode: EventNode) { 20 | Database.initialize(DatabaseConnectionImpl(Environment.mongoConnectionString)) 21 | Permissions.initialize(PermissionManagerImpl()) 22 | 23 | if (Environment.current.isDev) { 24 | logger.info("Using no-op stubs for messaging as this server is in a development environment.") 25 | Messaging.initializeIncoming(IncomingRPCHandlerStub()) 26 | Messaging.initializeOutgoing(OutgoingRPCHandlerStub()) 27 | } else { 28 | logger.info("Attempting to connect to messaging at address ${InetAddress.getByName(Environment.puffinHostname).hostAddress}") 29 | Messaging.initializeIncoming(IncomingRPCHandlerImpl(Environment.grpcServerPort)) 30 | Messaging.initializeOutgoing(OutgoingRPCHandlerImpl(Environment.puffinHostname, Environment.puffinPort)) 31 | runBlocking { 32 | Messaging.outgoing.initGameServer(Environment.getServerName()) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/PerInstanceChat.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import net.minestom.server.event.Event 4 | import net.minestom.server.event.EventFilter 5 | import net.minestom.server.event.EventNode 6 | import net.minestom.server.event.player.PlayerChatEvent 7 | import net.minestom.server.event.player.PlayerDeathEvent 8 | 9 | object PerInstanceChat : Bootstrap() { 10 | 11 | override fun hook(eventNode: EventNode) { 12 | eventNode.addListener(PlayerChatEvent::class.java) { event -> 13 | // Restrict player chat messages to only be visible in the player's current instance. 14 | event.recipients.removeAll { it.instance != event.player.instance } 15 | } 16 | 17 | val child = EventNode.event("per-instance-chat", EventFilter.ALL) { true } 18 | child.priority = Integer.MAX_VALUE // High priority; runs last 19 | eventNode.addChild(child) 20 | 21 | child.addListener(PlayerDeathEvent::class.java) { event -> 22 | // Cancel the regular death message and only send it to the dead player's instance. 23 | val message = event.chatMessage 24 | event.chatMessage = null 25 | if (message != null) { 26 | event.instance.sendMessage(message) 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/PerInstanceTabList.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import net.minestom.server.entity.Player 4 | import net.minestom.server.event.Event 5 | import net.minestom.server.event.EventNode 6 | import net.minestom.server.event.instance.AddEntityToInstanceEvent 7 | import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent 8 | import net.minestom.server.network.packet.server.play.PlayerInfoRemovePacket 9 | import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket 10 | import net.minestom.server.utils.PacketSendingUtils 11 | import java.util.* 12 | 13 | object PerInstanceTabList : Bootstrap() { 14 | override fun hook(eventNode: EventNode) { 15 | eventNode.addListener(AddEntityToInstanceEvent::class.java) { event -> 16 | if (event.entity !is Player) return@addListener 17 | val player = event.entity as Player 18 | val previousInstance = player.instance 19 | val newInstance = event.instance 20 | // Remove all players from the player's tablist (not necessary on first join) 21 | if (previousInstance != null) { 22 | player.sendPacket(getRemovePlayerPacket(previousInstance.players - player)) 23 | } 24 | // Add all the instance's current players to the joining player's tablist 25 | player.sendPacket( 26 | getAddPlayerPacket(newInstance.players) 27 | ) 28 | // Send a packet to all players in the instance to add this new player 29 | PacketSendingUtils.sendGroupedPacket( 30 | newInstance.players + player, 31 | getAddPlayerPacket(player) 32 | ) 33 | } 34 | eventNode.addListener(RemoveEntityFromInstanceEvent::class.java) { event -> 35 | if (event.entity !is Player) return@addListener 36 | val player = event.entity as Player 37 | // Remove this player from everyone's tablist 38 | PacketSendingUtils.sendGroupedPacket( 39 | event.instance.players - player, 40 | getRemovePlayerPacket(setOf(player)) 41 | ) 42 | } 43 | } 44 | 45 | private fun getAddPlayerPacket(players: Iterable) = 46 | PlayerInfoUpdatePacket( 47 | EnumSet.of( 48 | PlayerInfoUpdatePacket.Action.ADD_PLAYER, 49 | PlayerInfoUpdatePacket.Action.UPDATE_LISTED, 50 | PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME 51 | ), 52 | players.map { getAddPlayerEntry(it) }) 53 | 54 | private fun getRemovePlayerPacket(players: Iterable) = 55 | PlayerInfoRemovePacket(players.map { it.uuid }) 56 | 57 | private fun getAddPlayerPacket(player: Player) = 58 | PlayerInfoUpdatePacket( 59 | EnumSet.of( 60 | PlayerInfoUpdatePacket.Action.ADD_PLAYER, 61 | PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, 62 | PlayerInfoUpdatePacket.Action.UPDATE_LISTED 63 | ), 64 | listOf(getAddPlayerEntry(player)) 65 | ) 66 | 67 | private fun getAddPlayerEntry(player: Player) = PlayerInfoUpdatePacket.Entry( 68 | player.uuid, 69 | player.username, 70 | if (player.skin != null) listOf( 71 | PlayerInfoUpdatePacket.Property( 72 | "textures", 73 | player.skin!!.textures(), 74 | player.skin!!.signature() 75 | ) 76 | ) else emptyList(), 77 | true, 78 | player.latency, 79 | player.gameMode, 80 | player.name, 81 | null, 82 | 1024 83 | ) 84 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/ServerListPingHandler.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_3 4 | import com.bluedragonmc.server.api.Environment 5 | import com.bluedragonmc.server.FAVICON 6 | import com.bluedragonmc.server.SERVER_NAME_GRADIENT 7 | import com.bluedragonmc.server.utils.noBold 8 | import com.bluedragonmc.server.utils.plus 9 | import com.bluedragonmc.server.utils.withColor 10 | import net.kyori.adventure.text.Component 11 | import net.kyori.adventure.text.format.NamedTextColor 12 | import net.kyori.adventure.text.format.TextDecoration 13 | import net.minestom.server.event.Event 14 | import net.minestom.server.event.EventNode 15 | import net.minestom.server.event.server.ServerListPingEvent 16 | import net.minestom.server.ping.ServerListPingType 17 | 18 | object ServerListPingHandler : Bootstrap() { 19 | override fun hook(eventNode: EventNode) { 20 | 21 | eventNode.addListener(ServerListPingEvent::class.java) { event -> 22 | val versionString = Environment.versionInfo.run { "$BRANCH/$COMMIT" } 23 | val title = if (event.pingType != ServerListPingType.MODERN_FULL_RGB) 24 | Component.text("BlueDragon").withColor(BRAND_COLOR_PRIMARY_3) 25 | else SERVER_NAME_GRADIENT 26 | event.responseData.description = 27 | title.decorate(TextDecoration.BOLD) + 28 | Component.space() + 29 | Component.text("[", NamedTextColor.DARK_GRAY).noBold() + 30 | Component.text(versionString, NamedTextColor.GREEN).noBold() + 31 | Component.text("]", NamedTextColor.DARK_GRAY).noBold() 32 | event.responseData.favicon = FAVICON 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/TabListFormat.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap 2 | 3 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_1 4 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_2 5 | import com.bluedragonmc.server.SERVER_NAME_GRADIENT 6 | import net.kyori.adventure.text.Component 7 | import net.kyori.adventure.text.format.TextDecoration 8 | import net.minestom.server.event.Event 9 | import net.minestom.server.event.EventNode 10 | import net.minestom.server.event.player.PlayerSpawnEvent 11 | 12 | object TabListFormat : Bootstrap() { 13 | override fun hook(eventNode: EventNode) { 14 | eventNode.addListener(PlayerSpawnEvent::class.java) { event -> 15 | if (!event.isFirstSpawn) return@addListener 16 | 17 | event.player.sendPlayerListHeaderAndFooter(SERVER_NAME_GRADIENT.decorate(TextDecoration.BOLD), 18 | Component.translatable("global.tab.call_to_action", 19 | BRAND_COLOR_PRIMARY_2, 20 | Component.translatable("global.server.domain", BRAND_COLOR_PRIMARY_1))) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/dev/DevInstanceRouter.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap.dev 2 | 3 | import com.bluedragonmc.server.CustomPlayer 4 | import com.bluedragonmc.server.bootstrap.Bootstrap 5 | import com.bluedragonmc.server.bootstrap.prod.InitialInstanceRouter.DATA_LOAD_FAILED 6 | import com.bluedragonmc.server.isLobbyInitialized 7 | import com.bluedragonmc.server.lobby 8 | import com.bluedragonmc.server.module.minigame.SpawnpointModule 9 | import com.bluedragonmc.server.service.Database 10 | import com.bluedragonmc.server.utils.listen 11 | import kotlinx.coroutines.runBlocking 12 | import kotlinx.coroutines.withTimeout 13 | import net.minestom.server.MinecraftServer 14 | import net.minestom.server.event.Event 15 | import net.minestom.server.event.EventNode 16 | import net.minestom.server.event.instance.InstanceTickEvent 17 | import net.minestom.server.event.player.AsyncPlayerConfigurationEvent 18 | 19 | object DevInstanceRouter : Bootstrap(EnvType.DEVELOPMENT) { 20 | override fun hook(eventNode: EventNode) { 21 | eventNode.addListener(AsyncPlayerConfigurationEvent::class.java) { event -> 22 | try { 23 | runBlocking { 24 | withTimeout(5000) { 25 | Database.connection.loadDataDocument(event.player as CustomPlayer) 26 | } 27 | } 28 | } catch (e: Exception) { 29 | e.printStackTrace() 30 | event.player.kick(DATA_LOAD_FAILED) 31 | } 32 | 33 | if (isLobbyInitialized()) { 34 | // Send the player to the lobby 35 | event.spawningInstance = lobby.getInstance() 36 | lobby.players.add(event.player) 37 | val spawnpoint = 38 | lobby.getModuleOrNull()?.spawnpointProvider?.getSpawnpoint(event.player) 39 | if (spawnpoint != null) { 40 | event.player.respawnPoint = spawnpoint 41 | } 42 | } else { 43 | // Send the player to a temporary "limbo" instance while the lobby is being loaded 44 | val instance = MinecraftServer.getInstanceManager().createInstanceContainer() 45 | instance.enableAutoChunkLoad(false) 46 | instance.eventNode().listen { 47 | if (instance.players.isEmpty()) { 48 | MinecraftServer.getInstanceManager().unregisterInstance(instance) 49 | } 50 | if (isLobbyInitialized()) { 51 | lobby.addPlayer(event.player) 52 | } 53 | } 54 | event.spawningInstance = instance 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/dev/MojangAuthentication.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap.dev 2 | 3 | import com.bluedragonmc.server.bootstrap.Bootstrap 4 | import net.minestom.server.event.Event 5 | import net.minestom.server.event.EventNode 6 | import net.minestom.server.extras.MojangAuth 7 | 8 | object MojangAuthentication : Bootstrap() { 9 | override fun hook(eventNode: EventNode) { 10 | if (System.getenv("PUFFIN_VELOCITY_SECRET") == null) { 11 | MojangAuth.init() 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/dev/OpenToLAN.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap.dev 2 | 3 | import com.bluedragonmc.server.bootstrap.Bootstrap 4 | import net.minestom.server.event.Event 5 | import net.minestom.server.event.EventNode 6 | 7 | object OpenToLAN : Bootstrap(EnvType.DEVELOPMENT) { 8 | override fun hook(eventNode: EventNode) { 9 | net.minestom.server.extras.lan.OpenToLAN.open() 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/bootstrap/prod/VelocityForwarding.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.bootstrap.prod 2 | 3 | import com.bluedragonmc.server.api.Environment 4 | import com.bluedragonmc.server.bootstrap.Bootstrap 5 | import net.minestom.server.MinecraftServer 6 | import net.minestom.server.event.Event 7 | import net.minestom.server.event.EventNode 8 | import net.minestom.server.extras.velocity.VelocityProxy 9 | 10 | object VelocityForwarding : Bootstrap() { 11 | override fun hook(eventNode: EventNode) { 12 | if (System.getenv("PUFFIN_VELOCITY_SECRET") != null) { 13 | VelocityProxy.enable(System.getenv("PUFFIN_VELOCITY_SECRET").trim()) 14 | MinecraftServer.setCompressionThreshold(0) // Disable compression because packets are being proxied 15 | } else if (!Environment.current.isDev) { 16 | logger.warn("Warning: Running in a production-like environment without Velocity forwarding!") 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/FlyCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | class FlyCommand(name: String, vararg aliases: String) : BlueDragonCommand(name, aliases, block = { 4 | 5 | val playerArgument by PlayerArgument 6 | 7 | syntax { 8 | player.isAllowFlying = !player.isFlying 9 | player.isFlying = player.isAllowFlying 10 | if (player.isFlying) player.sendMessage(formatMessageTranslated("command.fly.own.flying")) 11 | else player.sendMessage(formatMessageTranslated("command.fly.own.not_flying")) 12 | }.requirePlayers() 13 | 14 | syntax(playerArgument) { 15 | val player = getFirstPlayer(playerArgument) 16 | player.isAllowFlying = !player.isFlying 17 | player.isFlying = player.isAllowFlying 18 | if (player.isFlying) player.sendMessage(formatMessageTranslated("command.fly.other.flying", player.name)) 19 | else player.sendMessage(formatMessageTranslated("command.fly.other.not_flying", player.name)) 20 | } 21 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/GameModeCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import net.kyori.adventure.text.Component 4 | import net.minestom.server.command.builder.arguments.ArgumentWord 5 | import net.minestom.server.entity.GameMode 6 | 7 | class GameModeCommand(name: String, usageString: String, vararg aliases: String) : BlueDragonCommand(name, aliases, block = { 8 | val gameModeArgument = ArgumentWord("gameMode").from("survival", "creative", "adventure", "spectator") 9 | val playerArgument by PlayerArgument 10 | 11 | usage(usageString) 12 | 13 | syntax(gameModeArgument) { 14 | val gameMode = get(gameModeArgument) 15 | player.gameMode = GameMode.valueOf(gameMode.uppercase()) 16 | sender.sendMessage(formatMessageTranslated("commands.gamemode.success.self", Component.translatable("gameMode.${gameMode.lowercase()}"))) 17 | }.requirePlayers() 18 | 19 | syntax(gameModeArgument, playerArgument) { 20 | val gameMode = get(gameModeArgument) 21 | val player = getFirstPlayer(playerArgument) 22 | player.gameMode = GameMode.valueOf(gameMode.uppercase()) 23 | sender.sendMessage(formatMessageTranslated("commands.gamemode.success.other", player.name, Component.translatable("gameMode.${gameMode.lowercase()}"))) 24 | } 25 | }) { 26 | 27 | open class SingleGameModeCommand(name: String, private val gameMode: GameMode) : BlueDragonCommand(name, permission = "command.gamemode", block = { 28 | val component = Component.translatable("gameMode.${gameMode.toString().lowercase()}") 29 | 30 | val otherArgument by PlayerArgument 31 | syntax { 32 | player.gameMode = gameMode 33 | player.sendMessage(formatMessageTranslated("commands.gamemode.success.self", component)) 34 | } 35 | syntax(otherArgument) { 36 | val other = getFirstPlayer(otherArgument) 37 | other.gameMode = gameMode 38 | sender.sendMessage(formatMessageTranslated("commands.gamemode.success.other", other.name, component)) 39 | } 40 | }) 41 | 42 | class GameModeSurvivalCommand : SingleGameModeCommand("gms", GameMode.SURVIVAL) 43 | class GameModeCreativeCommand : SingleGameModeCommand("gmc", GameMode.CREATIVE) 44 | class GameModeAdventureCommand : SingleGameModeCommand("gma", GameMode.ADVENTURE) 45 | class GameModeSpectatorCommand : SingleGameModeCommand("gmsp", GameMode.SPECTATOR) 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/GiveCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import net.kyori.adventure.text.Component 4 | 5 | class GiveCommand(name: String, usageString: String, vararg aliases: String) : 6 | BlueDragonCommand(name, aliases, block = { 7 | usage(usageString) 8 | 9 | val itemArgument by ItemStackArgument 10 | val playerArgument by PlayerArgument 11 | val amountArgument by IntArgument 12 | 13 | syntax(itemArgument) { 14 | val itemStack = get(itemArgument) 15 | 16 | player.inventory.addItemStack(itemStack) 17 | 18 | sender.sendMessage(formatMessageTranslated("command.give.self", 19 | 1, 20 | Component.translatable(itemStack.material().registry().translationKey()))) 21 | }.requirePlayers() 22 | 23 | syntax(itemArgument, amountArgument) { 24 | val itemStack = get(itemArgument) 25 | val amount = get(amountArgument) 26 | 27 | player.inventory.addItemStack(itemStack.withAmount(amount)) 28 | 29 | sender.sendMessage(formatMessageTranslated("command.give.self", 30 | amount, 31 | Component.translatable(itemStack.material().registry().translationKey()))) 32 | }.requirePlayers() 33 | 34 | syntax(playerArgument, itemArgument) { 35 | val player = getFirstPlayer(playerArgument) 36 | val itemStack = get(itemArgument) 37 | 38 | player.inventory.addItemStack(itemStack) 39 | 40 | sender.sendMessage(formatMessageTranslated("command.give.other", 41 | player.name, 42 | 1, 43 | Component.translatable(itemStack.material().registry().translationKey()))) 44 | } 45 | 46 | syntax(playerArgument, itemArgument, amountArgument) { 47 | val player = getFirstPlayer(playerArgument) 48 | val itemStack = get(itemArgument) 49 | val amount = get(amountArgument) 50 | 51 | player.inventory.addItemStack(itemStack.withAmount(amount)) 52 | 53 | sender.sendMessage(formatMessageTranslated("command.give.other", 54 | player.name, 55 | amount, 56 | Component.translatable(itemStack.material().registry().translationKey()))) 57 | } 58 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/JoinCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import com.bluedragonmc.api.grpc.CommonTypes.GameType.GameTypeFieldSelector 4 | import com.bluedragonmc.api.grpc.gameType 5 | import com.bluedragonmc.server.api.Environment 6 | import net.kyori.adventure.text.Component 7 | import net.kyori.adventure.text.format.NamedTextColor 8 | import net.minestom.server.command.builder.arguments.ArgumentWord 9 | 10 | class JoinCommand(name: String, private val usageString: String, vararg aliases: String) : BlueDragonCommand(name, aliases, block = { 11 | 12 | val gameArgument = ArgumentWord("game").from(*Environment.gameClasses.toTypedArray()) 13 | val modeArgument by WordArgument 14 | val mapArgument by WordArgument 15 | 16 | usage(usageString) 17 | 18 | syntax(gameArgument) { 19 | Environment.queue.queue(player, gameType { 20 | this.name = get(gameArgument) 21 | selectors += GameTypeFieldSelector.GAME_NAME 22 | }) 23 | }.requirePlayers() 24 | 25 | syntax(gameArgument, modeArgument) { 26 | Environment.queue.queue(player, gameType { 27 | this.name = get(gameArgument) 28 | this.mode = get(modeArgument) 29 | selectors += GameTypeFieldSelector.GAME_NAME 30 | selectors += GameTypeFieldSelector.GAME_MODE 31 | }) 32 | }.apply { 33 | requirePlayers() 34 | requirePermission("command.join.mode", Component.translatable("command.join.no_mode_permission", NamedTextColor.RED)) 35 | } 36 | 37 | syntax(gameArgument, modeArgument, mapArgument) { 38 | Environment.queue.queue(player, gameType { 39 | this.name = get(gameArgument) 40 | this.mode = get(modeArgument) 41 | this.mapName = get(mapArgument) 42 | selectors += GameTypeFieldSelector.GAME_NAME 43 | selectors += GameTypeFieldSelector.MAP_NAME 44 | selectors += GameTypeFieldSelector.GAME_MODE 45 | }) 46 | }.apply { 47 | requirePlayers() 48 | requirePermission("command.join.map", Component.translatable("command.join.no_map_permission", NamedTextColor.RED)) 49 | } 50 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/KillCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | class KillCommand(name: String, usageString: String, vararg aliases: String = emptyArray()) : 4 | BlueDragonCommand(name, aliases, block = { 5 | usage(usageString) 6 | 7 | syntax { 8 | player.kill() 9 | }.requirePlayers() 10 | 11 | val playerArgument by PlayerArgument 12 | syntax(playerArgument) { 13 | getFirstPlayer(playerArgument).kill() 14 | } 15 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/LeaderboardCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_1 4 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_2 5 | import com.bluedragonmc.server.Game 6 | import com.bluedragonmc.server.lobby 7 | import com.bluedragonmc.server.module.database.StatisticsModule 8 | import com.bluedragonmc.server.module.database.StatisticsModule.OrderBy 9 | import com.bluedragonmc.server.service.Database 10 | import com.bluedragonmc.server.service.Permissions 11 | import com.bluedragonmc.server.utils.plus 12 | import kotlinx.coroutines.launch 13 | import net.kyori.adventure.text.Component 14 | import net.minestom.server.entity.Player 15 | 16 | class LeaderboardCommand(name: String, usageString: String, vararg aliases: String) : BlueDragonCommand(name, aliases, block = { 17 | 18 | val keyArgument by WordArgument 19 | usage(usageString) 20 | 21 | syntax(keyArgument) { 22 | val key = get(keyArgument) 23 | val game = if (sender is Player) Game.findGame(player) ?: lobby else lobby 24 | Database.IO.launch { 25 | val leaderboard = game.getModule() 26 | .rankPlayersByStatistic(key, OrderBy.DESC, limit = 10) 27 | leaderboard.forEach { (doc, value) -> 28 | val color = Permissions.getMetadata(doc.uuid).rankColor 29 | val formattedName = Component.text(doc.username, color) 30 | sender.sendMessage(formattedName + Component.text(" - ", BRAND_COLOR_PRIMARY_2) + Component.text(value, BRAND_COLOR_PRIMARY_1)) 31 | } 32 | } 33 | } 34 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/ListCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_1 4 | import com.bluedragonmc.server.BRAND_COLOR_PRIMARY_2 5 | import com.bluedragonmc.server.service.Permissions 6 | import com.bluedragonmc.server.utils.plus 7 | import net.kyori.adventure.text.Component 8 | import net.kyori.adventure.text.JoinConfiguration 9 | import net.minestom.server.MinecraftServer 10 | import net.minestom.server.command.ConsoleSender 11 | import net.minestom.server.entity.Player 12 | import net.minestom.server.instance.Instance 13 | 14 | class ListCommand(name: String, vararg aliases: String) : BlueDragonCommand(name, aliases, block = { 15 | syntax { 16 | if (sender is ConsoleSender || Permissions.hasPermission((sender as Player).uuid, "command.list.full") == true) { 17 | val firstLine = formatMessageTranslated("command.list.response", 18 | MinecraftServer.getConnectionManager().onlinePlayers.size) 19 | val all = Component.join(JoinConfiguration.newlines(), 20 | MinecraftServer.getInstanceManager().instances.filter { it.players.isNotEmpty() }.map { instance -> 21 | Component.text(instance.uuid.toString(), BRAND_COLOR_PRIMARY_1) + Component.text(": ", 22 | BRAND_COLOR_PRIMARY_2) + getInstancePlayerList(instance) 23 | }) 24 | sender.sendMessage(firstLine + Component.newline() + all) 25 | } else { 26 | val instance = (sender as Player).instance!! 27 | val firstLine = formatMessageTranslated("command.list.response", instance.players.size) 28 | sender.sendMessage(firstLine + Component.space() + getInstancePlayerList(instance)) 29 | } 30 | } 31 | }) { 32 | companion object { 33 | internal fun getInstancePlayerList(instance: Instance) = buildMessage { 34 | instance.players.forEachIndexed { index, player -> 35 | component(player.name) 36 | if (index < instance.players.size - 1) message(", ") 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/LobbyCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import com.bluedragonmc.server.service.Messaging 4 | import com.bluedragonmc.server.lobby 5 | import com.bluedragonmc.server.module.minigame.SpawnpointModule 6 | 7 | class LobbyCommand(name: String, vararg aliases: String?) : BlueDragonCommand(name, aliases, block = { 8 | requirePlayers() 9 | suspendSyntax { 10 | if (player.instance == lobby.getInstance()) { 11 | val pos = lobby.getModuleOrNull()?.spawnpointProvider?.getSpawnpoint(player) 12 | if (pos != null) { 13 | player.teleport(pos) 14 | } else { 15 | player.sendMessage(formatErrorTranslated("command.lobby.already_in_lobby")) 16 | } 17 | return@suspendSyntax 18 | } 19 | player.setInstance( 20 | lobby.getInstance(), lobby.getModule().spawnpointProvider.getSpawnpoint(player) 21 | ) 22 | // Remove the player from the queue when they go to the lobby 23 | Messaging.outgoing.removeFromQueue(player) 24 | } 25 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/MessageCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import com.bluedragonmc.server.service.Messaging 4 | import com.bluedragonmc.server.service.Permissions 5 | import kotlinx.coroutines.runBlocking 6 | import net.kyori.adventure.text.Component 7 | import net.kyori.adventure.text.format.NamedTextColor 8 | import net.minestom.server.MinecraftServer 9 | import net.minestom.server.entity.Player 10 | import java.util.* 11 | 12 | class MessageCommand(name: String, vararg aliases: String) : BlueDragonCommand(name, aliases, block = { 13 | 14 | val playerArgument by OptionalPlayerArgument 15 | val messageArgument by StringArrayArgument 16 | 17 | syntax(playerArgument, messageArgument) { 18 | val playerName = get(playerArgument) 19 | val player = MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(playerName) 20 | val message = Component.text(get(messageArgument).joinToString(" "), NamedTextColor.GRAY) 21 | val senderName = (sender as? Player)?.name ?: Component.translatable("command.msg.console") 22 | if (player != null) { 23 | // Player is on the same server and online 24 | val color = Permissions.getMetadata(player.uuid).rankColor 25 | val senderMessage = formatMessageTranslated("command.msg.sent", Component.text(playerName, color), message) 26 | val receiverMessage = formatMessageTranslated("command.msg.received", senderName, message) 27 | sender.sendMessage(senderMessage) 28 | player.sendMessage(receiverMessage) 29 | } else { 30 | runBlocking { 31 | val recipient = Messaging.outgoing.queryPlayer(username = playerName) 32 | if (!recipient.isOnline) { 33 | sender.sendMessage(formatMessageTranslated("command.msg.fail", playerName)) 34 | return@runBlocking 35 | } 36 | val recipientUuid = UUID.fromString(recipient.uuid!!) 37 | val color = Permissions.getMetadata(recipientUuid).rankColor 38 | val senderMessage = formatMessageTranslated("command.msg.sent", Component.text(playerName, color), message) 39 | Messaging.outgoing.sendPrivateMessage(message, sender, recipientUuid) 40 | sender.sendMessage(senderMessage) 41 | } 42 | } 43 | } 44 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/MindecraftesCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import com.bluedragonmc.server.module.gameplay.NPCModule 4 | import com.bluedragonmc.server.utils.withColor 5 | import net.kyori.adventure.text.format.NamedTextColor 6 | import net.minestom.server.entity.PlayerSkin 7 | 8 | class MindecraftesCommand(name: String, usageString: String, vararg aliases: String) : BlueDragonCommand(name, aliases, block = { 9 | 10 | usage(usageString) 11 | requirePlayers() 12 | 13 | var isMindecraftes = false 14 | var previousSkin: PlayerSkin? = null 15 | syntax { 16 | if (player.uuid.toString() == "110429e8-197f-4446-8bec-5d66f17be4d5") { 17 | if (isMindecraftes) { 18 | player.skin = previousSkin 19 | player.displayName = player.username withColor (player.displayName?.color() ?: NamedTextColor.WHITE) 20 | isMindecraftes = false 21 | } else { 22 | previousSkin = player.skin 23 | player.skin = NPCModule.NPCSkins.MINDECRAFTES.skin 24 | player.displayName = "mindecraftes" withColor (player.displayName?.color() ?: NamedTextColor.WHITE) 25 | isMindecraftes = true 26 | } 27 | } else { 28 | player.sendMessage(formatErrorTranslated("command.mindecraftes.fail")) 29 | } 30 | } 31 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/PingCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | class PingCommand(name: String, usageString: String, vararg aliases: String) : 4 | BlueDragonCommand(name, aliases, block = { 5 | usage(usageString) 6 | 7 | syntax { 8 | sender.sendMessage(formatMessageTranslated("command.ping.response", player.latency)) 9 | }.requirePlayers() 10 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/PlaysoundCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import net.kyori.adventure.key.Key 4 | import net.kyori.adventure.sound.Sound 5 | import net.minestom.server.adventure.audience.PacketGroupingAudience 6 | import net.minestom.server.command.builder.arguments.ArgumentWord 7 | import net.minestom.server.command.builder.arguments.number.ArgumentFloat 8 | import net.minestom.server.coordinate.Pos 9 | import net.minestom.server.entity.Player 10 | import net.minestom.server.sound.SoundEvent 11 | 12 | class PlaysoundCommand(name: String, usageString: String, vararg aliases: String) : 13 | BlueDragonCommand(name, aliases, block = { 14 | val soundArgument = ArgumentWord("sound").from(*SoundEvent.values().map { it.name() }.toTypedArray()) 15 | val sourceArgument = ArgumentWord("source").from(*Sound.Source.values().map { it.name.lowercase() }.toTypedArray()) 16 | val targetArgument = ArgumentPlayer("target") 17 | val positionArgument by BlockPosArgument 18 | val volumeArgument = ArgumentFloat("volume").setDefaultValue(1.0f) 19 | val pitchArgument = ArgumentFloat("pitch").setDefaultValue(1.0f) 20 | 21 | usage(usageString) 22 | 23 | syntax(soundArgument, sourceArgument, targetArgument, positionArgument, volumeArgument, pitchArgument) { 24 | val targets = get(targetArgument).find(sender).filterIsInstance() 25 | val position = get(positionArgument).from((sender as? Player)?.position ?: Pos(0.0, 0.0, 0.0)) 26 | val audience = PacketGroupingAudience.of(targets) 27 | audience.playSound( 28 | Sound.sound( 29 | Key.key(get(soundArgument)), 30 | Sound.Source.valueOf(get(sourceArgument).uppercase()), 31 | get(volumeArgument), 32 | get(pitchArgument) 33 | ), 34 | position.x, 35 | position.y, 36 | position.z 37 | ) 38 | if (targets.size == 1) 39 | sender.sendMessage(formatMessageTranslated("commands.playsound.success.single", get(soundArgument), targets.first().name)) 40 | else 41 | sender.sendMessage(formatMessageTranslated("commands.playsound.success.multiple", get(soundArgument), targets.size)) 42 | } 43 | 44 | syntax(soundArgument, sourceArgument, targetArgument, volumeArgument, pitchArgument) { 45 | val targets = get(targetArgument).find(sender).filterIsInstance() 46 | val audience = PacketGroupingAudience.of(targets) 47 | audience.playSound( 48 | Sound.sound( 49 | Key.key(get(soundArgument)), 50 | Sound.Source.valueOf(get(sourceArgument).uppercase()), 51 | get(volumeArgument), 52 | get(pitchArgument) 53 | ) 54 | ) 55 | if (targets.size == 1) 56 | sender.sendMessage(formatMessageTranslated("commands.playsound.success.single", get(soundArgument), targets.first().name)) 57 | else 58 | sender.sendMessage(formatMessageTranslated("commands.playsound.success.multiple", get(soundArgument), targets.size)) 59 | } 60 | 61 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/SetBlockCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import net.kyori.adventure.text.Component 4 | 5 | class SetBlockCommand( 6 | name: String, 7 | usageString: String, 8 | vararg aliases: String = emptyArray(), 9 | ) : BlueDragonCommand(name, aliases, block = { 10 | 11 | usage(usageString) 12 | 13 | val positionArgument by BlockPosArgument 14 | val blockArgument by BlockStateArgument 15 | 16 | syntax(positionArgument, blockArgument) { 17 | val pos = get(positionArgument).from(player) 18 | val block = get(blockArgument) 19 | 20 | player.instance?.setBlock(pos, block) 21 | player.sendMessage( 22 | formatMessageTranslated( 23 | "command.setblock.response", 24 | formatPos(pos), 25 | Component.translatable(block.registry().translationKey()) 26 | ) 27 | ) 28 | }.requirePlayers() 29 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/StopCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import net.minestom.server.MinecraftServer 4 | import java.time.Duration 5 | import kotlin.system.exitProcess 6 | 7 | class StopCommand(name: String, usageString: String, vararg aliases: String?) : 8 | BlueDragonCommand(name, aliases, block = { 9 | 10 | val secondsArgument by IntArgument 11 | 12 | usage(usageString) 13 | 14 | syntax(secondsArgument) { 15 | sender.sendMessage(formatMessageTranslated("command.stop.response", get(secondsArgument))) 16 | MinecraftServer.getSchedulerManager().buildTask { 17 | MinecraftServer.stopCleanly() 18 | exitProcess(0) 19 | }.delay(Duration.ofSeconds(get(secondsArgument).toLong())).schedule() 20 | } 21 | 22 | syntax { 23 | MinecraftServer.stopCleanly() 24 | exitProcess(0) 25 | } 26 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/TeleportCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | /** 4 | * Usage: 5 | * /tp 6 | * /tp 7 | * /tp 8 | */ 9 | class TeleportCommand(name: String, usageString: String, vararg aliases: String?) : 10 | BlueDragonCommand(name, aliases, block = { 11 | val coordsArgument by BlockPosArgument 12 | val playerArgument by PlayerArgument 13 | val player2Argument by PlayerArgument 14 | 15 | usage(usageString) 16 | 17 | syntax(coordsArgument) { 18 | val pos = get(coordsArgument).fromSender(sender).asPosition() 19 | player.teleport(pos) 20 | sender.sendMessage(formatMessageTranslated("command.teleport.self", formatPos(pos))) 21 | }.requirePlayers() 22 | 23 | syntax(playerArgument) { 24 | val other = getFirstPlayer(playerArgument) 25 | player.teleport(other.position) 26 | sender.sendMessage(formatMessageTranslated("command.teleport.self", other.name)) 27 | }.requirePlayers() 28 | 29 | syntax(playerArgument, coordsArgument) { 30 | val other = getFirstPlayer(playerArgument) 31 | val pos = get(coordsArgument).fromSender(sender).asPosition() 32 | other.teleport(pos) 33 | sender.sendMessage(formatMessageTranslated("command.teleport.other", other.name, formatPos(pos))) 34 | } 35 | 36 | syntax(playerArgument, player2Argument) { 37 | val other1 = getFirstPlayer(playerArgument) 38 | val other2 = getFirstPlayer(player2Argument) 39 | other1.teleport(other2.position) 40 | sender.sendMessage(formatMessageTranslated("command.teleport.other", other1.name, other2.name)) 41 | } 42 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/bluedragonmc/server/command/TimeCommand.kt: -------------------------------------------------------------------------------- 1 | package com.bluedragonmc.server.command 2 | 3 | import net.minestom.server.command.builder.arguments.ArgumentWord 4 | 5 | class TimeCommand(name: String, usageString: String, vararg aliases: String?) : 6 | BlueDragonCommand(name, aliases, block = { 7 | val timeArgument by IntArgument 8 | val timeRateArgument by IntArgument 9 | val timePresetArgument = ArgumentWord("time").from("day", "night", "noon", "midnight", "sunrise", "sunset") 10 | 11 | usage(usageString) 12 | 13 | syntax { 14 | player.sendMessage(formatMessageTranslated("commands.time.query", player.instance!!.time)) 15 | } 16 | 17 | subcommand("add") { 18 | usage("/time add