diff --git a/.github/workflows/gradle.yml b/.github/workflows/build.yml
similarity index 64%
rename from .github/workflows/gradle.yml
rename to .github/workflows/build.yml
index fb6c44d..daf85c5 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/build.yml
@@ -21,22 +21,22 @@ jobs:
contents: read
steps:
- - uses: actions/checkout@v4
- - name: Set up JDK 21
- uses: actions/setup-java@v4
- with:
- java-version: '21'
- distribution: 'temurin'
-
- # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
- # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
-
- - name: Make gradlew executable
- run: chmod +x ./gradlew
- - name: Build with Gradle Wrapper
- run: ./gradlew build
+ - uses: actions/checkout@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+
+ # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
+ # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
+
+ - name: Make gradlew executable
+ run: chmod +x ./gradlew
+ - name: Build with Gradle Wrapper
+ run: ./gradlew build
# NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html).
# If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version.
@@ -47,4 +47,4 @@ jobs:
# gradle-version: '8.9'
#
# - name: Build with Gradle 8.9
- # run: gradle build
+ # run: gradle build
\ No newline at end of file
diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 0000000..15c9952
--- /dev/null
+++ b/api/build.gradle
@@ -0,0 +1,16 @@
+plugins {
+ id 'java-library'
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation platform('org.junit:junit-bom:5.10.0')
+ testImplementation 'org.junit.jupiter:junit-jupiter'
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/bastion/README.md b/bastion/README.md
new file mode 100644
index 0000000..ade6867
--- /dev/null
+++ b/bastion/README.md
@@ -0,0 +1,5 @@
+
+

+
+
The best Minecraft AntiCheat.
+
\ No newline at end of file
diff --git a/bastion/bastion.png b/bastion/bastion.png
new file mode 100644
index 0000000..6f75bf4
Binary files /dev/null and b/bastion/bastion.png differ
diff --git a/bastion/build.gradle.kts b/bastion/build.gradle.kts
new file mode 100644
index 0000000..f9f5a0e
--- /dev/null
+++ b/bastion/build.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+ kotlin("jvm")
+ id("io.papermc.paperweight.userdev")
+}
+
+repositories {
+ mavenCentral()
+ maven("https://repo.papermc.io/repository/maven-public/")
+}
+
+dependencies {
+ paperweight.foliaDevBundle("1.21.4-R0.1-SNAPSHOT")
+}
\ No newline at end of file
diff --git a/bastion/src/main/resources/paper-plugin.yml b/bastion/src/main/resources/paper-plugin.yml
new file mode 100644
index 0000000..16a44f8
--- /dev/null
+++ b/bastion/src/main/resources/paper-plugin.yml
@@ -0,0 +1,4 @@
+name: "Bastion"
+version: "${version}"
+main: "com.otfhee.bastion.Bastion"
+api-version: "1.21.1"
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index eea52ba..d9b2636 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,57 +1,26 @@
plugins {
- id 'java'
- id 'io.papermc.paperweight.userdev' version '1.7.2'
- id 'xyz.jpenilla.run-paper' version '2.3.0'
+ id 'com.github.johnrengelman.shadow' version '8.1.1'
+ id 'io.papermc.paperweight.userdev' version '2.0.0-beta.11' apply false
+ id 'org.jetbrains.kotlin.jvm' version '2.1.10' apply false
}
-group = 'com.otfhee'
-version = '1.2.2'
+subprojects {
+ apply plugin: 'java'
+ apply plugin: 'maven-publish'
-compileJava.options.encoding = 'UTF-8'
+ group = 'com.otfhee'
+ version = '1.3.0-SNAPSHOT'
-repositories {
- mavenCentral()
- maven {
- name = "papermc-repo"
- url = "https://repo.papermc.io/repository/maven-public/"
- }
- maven {
- name = "sonatype"
- url = "https://oss.sonatype.org/content/groups/public/"
- }
- maven {
- name = "codemc-repo"
- url = "https://repo.codemc.org/repository/maven-public/"
- }
-}
-
-dependencies {
- paperweight.paperDevBundle('1.21.1-R0.1-SNAPSHOT')
- compileOnly "dev.jorel:commandapi-bukkit-core:9.4.1"
-}
+ project.ext.version = version
-def targetJavaVersion = 21
-java {
- def javaVersion = JavaVersion.toVersion(targetJavaVersion)
- sourceCompatibility = javaVersion
- targetCompatibility = javaVersion
- if (JavaVersion.current() < javaVersion) {
- toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
- }
-}
+ sourceCompatibility = 21
+ targetCompatibility = 21
-tasks.withType(JavaCompile).configureEach {
- if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
- options.release.set(targetJavaVersion)
+ tasks.withType(JavaCompile).configureEach {
+ options.encoding = 'UTF-8'
}
- options.encoding = 'UTF-8'
-}
-processResources {
- def props = [version: version]
- inputs.properties props
- filteringCharset 'UTF-8'
- filesMatching('plugin.yml') {
- expand props
+ repositories {
+ mavenCentral()
}
-}
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index e69de29..af82e00 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -0,0 +1 @@
+org.gradle.parallel=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 1af9e09..cea7a79 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/paper/build.gradle b/paper/build.gradle
new file mode 100644
index 0000000..f4dbba9
--- /dev/null
+++ b/paper/build.gradle
@@ -0,0 +1,69 @@
+plugins {
+ id 'java-library'
+ id 'io.papermc.paperweight.userdev'
+ id 'xyz.jpenilla.run-paper' version '2.3.0'
+ id "com.github.johnrengelman.shadow" version "8.1.1"
+ id 'org.jetbrains.kotlin.jvm' version '2.1.10'
+}
+
+repositories {
+ maven { url 'https://repo.papermc.io/repository/maven-public/' }
+ maven { url 'https://nexus.scarsz.me/content/groups/public/' }
+ maven { url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' }
+ maven { url = 'https://repo.codemc.org/repository/maven-public/' }
+ maven { url "https://repo.dmulloy2.net/repository/public/" }
+ mavenCentral()
+}
+
+processResources {
+ filesMatching('paper-plugin.yml') {
+ expand 'version': project.ext.version
+ }
+}
+
+dependencies {
+ implementation project(":api")
+
+ implementation 'org.jetbrains.exposed:exposed-core:0.59.0'
+ implementation 'org.jetbrains.exposed:exposed-dao:0.59.0'
+ implementation 'org.jetbrains.exposed:exposed-jdbc:0.59.0'
+ implementation 'org.jetbrains.exposed:exposed-kotlin-datetime:0.59.0'
+
+ implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1'
+
+ implementation 'org.mariadb.jdbc:mariadb-java-client:3.5.2'
+ implementation 'mysql:mysql-connector-java:8.0.33'
+ implementation 'org.xerial:sqlite-jdbc:3.42.0.0'
+
+ paperweight.foliaDevBundle('1.21.4-R0.1-SNAPSHOT')
+
+ compileOnly 'dev.jorel:commandapi-bukkit-core:9.7.0'
+ compileOnly 'net.luckperms:api:5.4'
+ compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
+
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
+}
+
+
+shadowJar {
+ archiveFileName = "OTFHEE-Paper-${project.ext.version}.jar"
+
+ mergeServiceFiles()
+
+ dependencies {
+ include(dependency('com.otfhee:.*') as Spec super ResolvedDependency>)
+ include(dependency('org.jetbrains.exposed:.*') as Spec super ResolvedDependency>)
+ include(dependency('org.jetbrains.kotlin:.*') as Spec super ResolvedDependency>)
+ include(dependency('org.jetbrains.kotlinx:kotlinx-datetime-jvm:.*') as Spec super ResolvedDependency>)
+ include(dependency('org.mariadb.jdbc:mariadb-java-client') as Spec super ResolvedDependency>)
+ include(dependency('mysql:mysql-connector-java') as Spec super ResolvedDependency>)
+ }
+}
+
+artifacts {
+ archives shadowJar
+}
+kotlin {
+ jvmToolchain(21)
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/Plugin.kt b/paper/src/main/kotlin/com/otfhee/paper/Plugin.kt
new file mode 100644
index 0000000..2196b1d
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/Plugin.kt
@@ -0,0 +1,261 @@
+package com.otfhee.paper
+
+import com.comphenix.protocol.ProtocolLibrary
+import com.comphenix.protocol.ProtocolManager
+import com.otfhee.paper.chat.ChatManager
+import com.otfhee.paper.commands.action.CoinCommand
+import com.otfhee.paper.commands.action.DoCommand
+import com.otfhee.paper.commands.action.MeCommand
+import com.otfhee.paper.commands.action.TryCommand
+import com.otfhee.paper.commands.chat.GlobalModeCommand
+import com.otfhee.paper.commands.chat.PrivateMessagesCommand
+import com.otfhee.paper.commands.display.DisplayCommand
+import com.otfhee.paper.commands.security.*
+import com.otfhee.paper.commands.social.DiscordCommand
+import com.otfhee.paper.commands.social.RulesCommand
+import com.otfhee.paper.commands.social.SiteCommand
+import com.otfhee.paper.commands.social.TelegramCommand
+import com.otfhee.paper.commands.util.LanguageCommand
+import com.otfhee.paper.commands.util.PingCommand
+import com.otfhee.paper.commands.util.PremiumCommand
+import com.otfhee.paper.commands.util.StatusCommand
+import com.otfhee.paper.database.ban.Bans
+import com.otfhee.paper.database.mute.Mutes
+import com.otfhee.paper.database.user.Users
+import com.otfhee.paper.database.warn.Warns
+import com.otfhee.paper.listeners.*
+import com.otfhee.paper.recipes.DebugStickRecipe
+import com.otfhee.paper.schedulers.AutoRestart
+import com.otfhee.paper.schedulers.PlayerList
+import com.otfhee.paper.schedulers.PremiumChecker
+import com.otfhee.paper.utils.PetUtil
+import dev.jorel.commandapi.CommandAPI
+import net.luckperms.api.LuckPerms
+import net.luckperms.api.LuckPermsProvider
+import org.bukkit.event.world.SpawnChangeEvent
+import org.bukkit.plugin.java.JavaPlugin
+import org.jetbrains.exposed.sql.Database
+import org.jetbrains.exposed.sql.SchemaUtils
+import org.jetbrains.exposed.sql.transactions.transaction
+import org.sqlite.SQLiteDataSource
+import java.io.File
+
+class Plugin : JavaPlugin() {
+ private lateinit var provider: LuckPerms
+ private var lastModified: Long = 0
+ private lateinit var protocolManager: ProtocolManager
+ private lateinit var configFile: File
+ private lateinit var petUtil: PetUtil
+ private lateinit var chatManager: ChatManager
+
+ private lateinit var antiGriefListener: AntiGriefListener
+ private lateinit var loginListener: LoginListener
+ private lateinit var playerHeadDropListener: PlayerHeadDropListener
+ private lateinit var chatListener: ChatListener
+ private lateinit var statusListener: StatusListener
+ private lateinit var banListener: BanListener
+ private lateinit var anvilListener: AnvilListener
+ private lateinit var playtimeListener: PlaytimeListener
+ private lateinit var invisibleFrameListener: InvisibleFrameListener
+ private lateinit var phantomSpawnListener: PhantomSpawnListener
+ private lateinit var resourcePackDamageListener: ResourcePackDamageListener
+ private lateinit var boatUphillListener: BoatUphillListener
+ private lateinit var leashListener: LeashListener
+ private lateinit var blockHeadListener: BlockHeadListener
+ private lateinit var petListener: PetListener
+ private lateinit var allayInteractListener: AllayInteractListener
+ private lateinit var debugStickInteractListener: DebugStickInteractListener
+
+ private lateinit var autoRestart: AutoRestart
+ private lateinit var playerList: PlayerList
+ private lateinit var premiumChecker: PremiumChecker
+
+ private lateinit var displayCommand: DisplayCommand
+ private lateinit var statusCommand: StatusCommand
+ private lateinit var languageCommand: LanguageCommand
+ private lateinit var banCommand: BanCommand
+ private lateinit var unbanCommand: UnbanCommand
+ private lateinit var privateMessagesCommand: PrivateMessagesCommand
+ private lateinit var coinCommand: CoinCommand
+ private lateinit var globalModeCommand: GlobalModeCommand
+ private lateinit var doCommand: DoCommand
+ private lateinit var meCommand: MeCommand
+ private lateinit var tryCommand: TryCommand
+ private lateinit var discordCommand: DiscordCommand
+ private lateinit var rulesCommand: RulesCommand
+ private lateinit var siteCommand: SiteCommand
+ private lateinit var telegramCommand: TelegramCommand
+ private lateinit var pingCommand: PingCommand
+ private lateinit var warnCommand: WarnCommand
+ private lateinit var premiumCommand: PremiumCommand
+ private lateinit var muteCommand: MuteCommand
+ private lateinit var opCommand: OpCommand
+
+ private lateinit var debugStickRecipe: DebugStickRecipe
+
+ override fun onLoad() {
+ super.onLoad()
+ protocolManager = ProtocolLibrary.getProtocolManager()
+ }
+
+ override fun onEnable() {
+ super.onEnable()
+ saveDefaultConfig()
+ connectDB()
+
+ provider = LuckPermsProvider.get()
+ petUtil = PetUtil(this)
+ chatManager = ChatManager(this, provider)
+
+ logger.info("Loading commands...")
+ CommandAPI.onEnable()
+ displayCommand = DisplayCommand(this, provider, petUtil)
+ statusCommand = StatusCommand(this)
+ languageCommand = LanguageCommand(this)
+ banCommand = BanCommand(this)
+ unbanCommand = UnbanCommand(this)
+ privateMessagesCommand = PrivateMessagesCommand(this, chatManager)
+ coinCommand = CoinCommand(this)
+ globalModeCommand = GlobalModeCommand(this)
+ doCommand = DoCommand(this)
+ meCommand = MeCommand(this)
+ tryCommand = TryCommand(this)
+ discordCommand = DiscordCommand(this)
+ rulesCommand = RulesCommand(this)
+ siteCommand = SiteCommand(this)
+ telegramCommand = TelegramCommand(this)
+ pingCommand = PingCommand(this)
+ warnCommand = WarnCommand(this)
+ premiumCommand = PremiumCommand(this, provider)
+ muteCommand = MuteCommand(this)
+ opCommand = OpCommand()
+
+ configFile = File(dataFolder, "config.yml")
+ lastModified = configFile.lastModified()
+
+ logger.info("Loading recipes...")
+ debugStickRecipe = DebugStickRecipe(this)
+
+ server.addRecipe(debugStickRecipe.recipe())
+
+ logger.info("Loading events...")
+ antiGriefListener = AntiGriefListener(this)
+ loginListener = LoginListener(this, chatManager)
+ playerHeadDropListener = PlayerHeadDropListener(this)
+ chatListener = ChatListener(this, chatManager, provider)
+ statusListener = StatusListener(this)
+ banListener = BanListener(this)
+ anvilListener = AnvilListener()
+ playtimeListener = PlaytimeListener(this)
+ invisibleFrameListener = InvisibleFrameListener()
+ phantomSpawnListener = PhantomSpawnListener(this)
+ resourcePackDamageListener = ResourcePackDamageListener()
+ boatUphillListener = BoatUphillListener(this)
+ leashListener = LeashListener()
+ blockHeadListener = BlockHeadListener()
+ petListener = PetListener(petUtil, provider)
+ allayInteractListener = AllayInteractListener()
+ debugStickInteractListener = DebugStickInteractListener()
+
+ server.pluginManager.registerEvents(antiGriefListener, this)
+ server.pluginManager.registerEvents(loginListener, this)
+ server.pluginManager.registerEvents(playerHeadDropListener, this)
+ server.pluginManager.registerEvents(chatListener, this)
+ server.pluginManager.registerEvents(statusListener, this)
+ server.pluginManager.registerEvents(banListener, this)
+ server.pluginManager.registerEvents(anvilListener, this)
+ server.pluginManager.registerEvents(playtimeListener, this)
+ server.pluginManager.registerEvents(invisibleFrameListener, this)
+ server.pluginManager.registerEvents(phantomSpawnListener, this)
+ server.pluginManager.registerEvents(resourcePackDamageListener, this)
+ server.pluginManager.registerEvents(boatUphillListener, this)
+ server.pluginManager.registerEvents(leashListener, this)
+ server.pluginManager.registerEvents(blockHeadListener, this)
+ server.pluginManager.registerEvents(petListener, this)
+ server.pluginManager.registerEvents(allayInteractListener, this)
+ server.pluginManager.registerEvents(debugStickInteractListener, this)
+
+ logger.info("Starting threads...")
+ autoRestart = AutoRestart(this)
+ premiumChecker = PremiumChecker(this, provider)
+ playerList = PlayerList(this)
+ server.globalRegionScheduler.runAtFixedRate(this, { playtimeListener.run() }, 1L, 20L)
+ server.globalRegionScheduler.runAtFixedRate(this, { autoRestart.run() }, 1L, 20L)
+ server.globalRegionScheduler.runAtFixedRate(this, { playerList.run() }, 1L, 20L)
+ server.globalRegionScheduler.runAtFixedRate(this, { statusListener.run() }, 1L, 20L)
+ server.globalRegionScheduler.runAtFixedRate(this, { premiumChecker.run() }, 1L, 20L)
+ server.globalRegionScheduler.runAtFixedRate(this, { checkConfigChanges() }, 20L * 5, 20L * 5)
+ }
+
+ override fun onDisable() {
+ super.onDisable()
+ CommandAPI.onDisable()
+ petUtil.removeAllPets()
+ server.globalRegionScheduler.cancelTasks(this)
+ }
+
+ private fun connectDB() {
+ when(val uri: String? = config.getString("database.url")) {
+ null -> {
+ logger.warning("URL database is empty!")
+ server.pluginManager.disablePlugin(this)
+ }
+ "jdbc:sqlite:database.db" -> {
+ Database.connect(SQLiteDataSource().apply {
+ url = "jdbc:sqlite:database.db"
+ })
+ }
+ else -> {
+ val driver = when {
+ uri.contains("jdbc:mysql") -> "com.mysql.cj.jdbc.Driver"
+ else -> { "org.mariadb.jdbc.Driver" }
+ }
+
+ try {
+ Database.connect(
+ "$uri?createDatabaseIfNotExist=true",
+ driver = driver,
+ user = config.getString("database.user").toString(),
+ password = config.getString("database.password").toString(),
+ setupConnection = { connection ->
+ connection.autoCommit = false
+ }
+ )
+ logger.info("Database connected!")
+ } catch (exception: Exception) {
+ logger.warning("Database cant connected: $exception")
+ server.pluginManager.disablePlugin(this)
+ }
+ }
+ }
+
+ transaction {
+ SchemaUtils.create(
+ Users,
+ Bans,
+ Warns,
+ Mutes
+ )
+ }
+ }
+
+ private fun checkConfigChanges() {
+ val currentModified = configFile.lastModified()
+
+ if (currentModified > lastModified) {
+ server.globalRegionScheduler.run(this) { _ ->
+ reloadConfig()
+ logger.info("Configuration has been reloaded!")
+ }
+ lastModified = currentModified
+ }
+ }
+
+ fun luckPerms(): LuckPerms {
+ return provider
+ }
+
+ fun protocolLib(): ProtocolManager {
+ return protocolManager
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/ChatManager.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/ChatManager.kt
new file mode 100644
index 0000000..1291513
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/ChatManager.kt
@@ -0,0 +1,61 @@
+package com.otfhee.paper.chat
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.chat.processors.*
+import com.otfhee.paper.chat.renders.GlobalChatRenderer
+import com.otfhee.paper.chat.renders.LocalChatRenderer
+import com.otfhee.paper.utils.MuteUtil
+import net.kyori.adventure.audience.Audience
+import net.luckperms.api.LuckPerms
+import org.bukkit.entity.Player
+import java.util.concurrent.CopyOnWriteArrayList
+
+class ChatManager(
+ private val plugin: Plugin,
+ luckPerms: LuckPerms
+) {
+ private val messageProcessors = CopyOnWriteArrayList()
+ private val muteUtil = MuteUtil(plugin)
+
+ init {
+ registerProcessor(URLProcessor(plugin))
+ registerProcessor(CoordinatesProcessor())
+ registerProcessor(MentionProcessor())
+ registerProcessor(FormattingProcessor(luckPerms))
+ registerProcessor(DiceProcessor())
+ }
+
+ fun registerProcessor(processor: MessageProcessor) {
+ messageProcessors.add(processor)
+ }
+
+ fun unregisterProcessor(processor: MessageProcessor) {
+ messageProcessors.remove(processor)
+ }
+
+ fun processMessage(message: String, player: Player, viewer: Audience? = null): String {
+ var processedMessage = message
+
+ for (processor in messageProcessors) {
+ processedMessage = processor.process(processedMessage, player, viewer)
+ }
+
+ return processedMessage
+ }
+
+ fun getGlobalChatRenderer(): GlobalChatRenderer {
+ return GlobalChatRenderer(plugin, this)
+ }
+
+ fun getLocalChatRenderer(): LocalChatRenderer {
+ return LocalChatRenderer(plugin, this)
+ }
+
+ fun isMuted(player: Player): Boolean {
+ return muteUtil.isMute(player)
+ }
+
+ fun sendMuteMessage(player: Player) {
+ muteUtil.muteMessage(player)
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/processors/CoordinatesProcessor.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/CoordinatesProcessor.kt
new file mode 100644
index 0000000..00f38d2
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/CoordinatesProcessor.kt
@@ -0,0 +1,18 @@
+package com.otfhee.paper.chat.processors
+
+import net.kyori.adventure.audience.Audience
+import org.bukkit.entity.Player
+
+class CoordinatesProcessor : MessageProcessor {
+ override fun process(message: String, player: Player, viewer: Audience?): String {
+ var processedMessage = message
+ val coordinates = "${"%.1f".format(player.x)} ${"%.1f".format(player.y)} ${"%.1f".format(player.z)}"
+
+ val coordVariations = listOf(":coords:", ":coord:", ":cord:", ":cords:", ":coordinate:", ":coordinates:")
+ for (variation in coordVariations) {
+ processedMessage = processedMessage.replace(variation, coordinates)
+ }
+
+ return processedMessage
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/processors/DiceProcessor.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/DiceProcessor.kt
new file mode 100644
index 0000000..a4a24f4
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/DiceProcessor.kt
@@ -0,0 +1,16 @@
+package com.otfhee.paper.chat.processors
+
+import net.kyori.adventure.audience.Audience
+import org.bukkit.entity.Player
+
+class DiceProcessor : MessageProcessor {
+ override fun process(message: String, player: Player, viewer: Audience?): String {
+ var processedMessage = message
+
+ val dices = listOf("⚅", "⚄", "⚃", "⚂", "⚁", "⚀")
+
+ processedMessage = processedMessage.replace(Regex(":dice:")) { dices.random() }
+
+ return processedMessage
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/processors/FormattingProcessor.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/FormattingProcessor.kt
new file mode 100644
index 0000000..bb989ca
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/FormattingProcessor.kt
@@ -0,0 +1,15 @@
+package com.otfhee.paper.chat.processors
+
+import net.kyori.adventure.audience.Audience
+import net.luckperms.api.LuckPerms
+import org.bukkit.entity.Player
+
+class FormattingProcessor(private val luckPerms: LuckPerms) : MessageProcessor {
+ override fun process(message: String, player: Player, viewer: Audience?): String {
+ val lp = luckPerms.userManager.loadUser(player.uniqueId)
+
+ val decoration = lp.get().cachedData.metaData.getMetaValue("text.decoration") ?: ""
+ val color = lp.get().cachedData.metaData.getMetaValue("text.color") ?: ""
+ return "$decoration$color$message"
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/processors/MentionProcessor.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/MentionProcessor.kt
new file mode 100644
index 0000000..4d5298b
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/MentionProcessor.kt
@@ -0,0 +1,54 @@
+package com.otfhee.paper.chat.processors
+
+import com.otfhee.paper.database.user.User
+import com.otfhee.paper.database.user.UserStatus
+import net.kyori.adventure.audience.Audience
+import net.kyori.adventure.key.Key
+import net.kyori.adventure.sound.Sound
+import org.bukkit.entity.Player
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class MentionProcessor : MessageProcessor {
+ private val mentionPattern = Regex("@(\\w+)")
+
+ override fun process(message: String, player: Player, viewer: Audience?): String {
+ var processedMessage = message
+ val mentionedPlayers = mutableListOf()
+
+ val matches = mentionPattern.findAll(message).toList()
+
+ for (match in matches) {
+ val mentionedName = match.groupValues[1]
+ val mentionedPlayer = player.server.getPlayerExact(mentionedName)
+
+ if (mentionedPlayer != null && mentionedPlayer.isOnline) {
+ mentionedPlayers.add(mentionedPlayer)
+
+ val formattedMention = if (viewer is Player && viewer.name.equals(mentionedName, ignoreCase = true)) {
+ "@$mentionedName"
+ } else {
+ "@$mentionedName"
+ }
+
+ processedMessage = processedMessage.replace(match.value, formattedMention)
+ }
+ }
+
+ for (mentionedPlayer in mentionedPlayers) {
+ val user = transaction {
+ User[mentionedPlayer.uniqueId]
+ }
+
+ if (user.status != UserStatus.DO_NOT_DISTURB.name) {
+ mentionedPlayer.playSound(
+ Sound.sound(
+ Key.key("minecraft:entity.experience_orb.pickup"),
+ Sound.Source.MASTER, 1f, 1f
+ )
+ )
+ }
+ }
+
+ return processedMessage
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/processors/MessageProcessor.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/MessageProcessor.kt
new file mode 100644
index 0000000..85d5a43
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/MessageProcessor.kt
@@ -0,0 +1,8 @@
+package com.otfhee.paper.chat.processors
+
+import net.kyori.adventure.audience.Audience
+import org.bukkit.entity.Player
+
+interface MessageProcessor {
+ fun process(message: String, player: Player, viewer: Audience? = null): String
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/processors/URLProcessor.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/URLProcessor.kt
new file mode 100644
index 0000000..ddad806
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/processors/URLProcessor.kt
@@ -0,0 +1,49 @@
+package com.otfhee.paper.chat.processors
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.utils.LanguageUtil
+import net.kyori.adventure.audience.Audience
+import org.bukkit.entity.Player
+
+class URLProcessor(plugin: Plugin) : MessageProcessor {
+ private val urlPattern = java.util.regex.Pattern.compile(
+ "((https?|ftp)://|(www|ftp)\\.)?[a-z0-9-]+(\\.[a-z0-9-]+)+([/?][^\\s<>\"']*)?",
+ java.util.regex.Pattern.CASE_INSENSITIVE
+ )
+
+ private val languageUtil = LanguageUtil(plugin)
+
+ override fun process(message: String, player: Player, viewer: Audience?): String {
+ val matcher = urlPattern.matcher(message)
+ val builder = StringBuilder()
+ var lastEnd = 0
+
+ while (matcher.find()) {
+ val url = matcher.group()
+
+ if (url.matches(Regex("^\\d+\\.\\d+(\\.\\d+)?$"))) {
+ builder.append(message.substring(lastEnd, matcher.end()))
+ lastEnd = matcher.end()
+ continue
+ }
+
+ builder.append(message.substring(lastEnd, matcher.start()))
+
+ val displayUrl = if (url.length > 40) {
+ val halfLength = 20
+ "${url.take(halfLength)}...${url.takeLast(halfLength)}"
+ } else {
+ url
+ }
+
+ builder.append("${languageUtil.getMessage(viewer ?: player, "chat.link")}\n$url'>$displayUrl")
+ lastEnd = matcher.end()
+ }
+
+ if (lastEnd < message.length) {
+ builder.append(message.substring(lastEnd))
+ }
+
+ return if (builder.isEmpty()) message else builder.toString()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/renders/GlobalChatRenderer.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/renders/GlobalChatRenderer.kt
new file mode 100644
index 0000000..8b2038f
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/renders/GlobalChatRenderer.kt
@@ -0,0 +1,33 @@
+package com.otfhee.paper.chat.renders
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.chat.ChatManager
+import com.otfhee.paper.messenger.PastelMessage
+import net.kyori.adventure.audience.Audience
+import net.kyori.adventure.text.Component
+import org.bukkit.entity.Player
+
+class GlobalChatRenderer(
+ plugin: Plugin,
+ chatManager: ChatManager
+) : MainChatRenderer(plugin, chatManager) {
+
+ override fun getChatFormat(): String {
+ return plugin.config.getString("chat.style.global-mode") ?: "[G] : "
+ }
+
+ override fun render(source: Player, sourceDisplayName: Component, message: Component, viewer: Audience): Component {
+ var messageText = miniMessage.serialize(message)
+ if (messageText.startsWith("!")) {
+ messageText = messageText.substring(1)
+ }
+
+ if (messageText.startsWith(" ")) {
+ messageText = messageText.substring(1)
+ }
+
+ return super.render(source, sourceDisplayName,
+ PastelMessage.survivalOf(messageText),
+ viewer)
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/renders/LocalChatRenderer.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/renders/LocalChatRenderer.kt
new file mode 100644
index 0000000..094d56a
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/renders/LocalChatRenderer.kt
@@ -0,0 +1,18 @@
+package com.otfhee.paper.chat.renders
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.chat.ChatManager
+import com.otfhee.paper.messenger.PastelMessage
+import net.kyori.adventure.audience.Audience
+import net.kyori.adventure.text.Component
+import org.bukkit.entity.Player
+
+class LocalChatRenderer(
+ plugin: Plugin,
+ chatManager: ChatManager,
+) : MainChatRenderer(plugin, chatManager) {
+
+ override fun getChatFormat(): String {
+ return plugin.config.getString("chat.style.local-mode") ?: ": "
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/chat/renders/MainChatRenderer.kt b/paper/src/main/kotlin/com/otfhee/paper/chat/renders/MainChatRenderer.kt
new file mode 100644
index 0000000..98f25b7
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/chat/renders/MainChatRenderer.kt
@@ -0,0 +1,39 @@
+package com.otfhee.paper.chat.renders
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.chat.ChatManager
+import com.otfhee.paper.display.DisplayName
+import com.otfhee.paper.messenger.PastelMessage
+import io.papermc.paper.chat.ChatRenderer
+import net.kyori.adventure.audience.Audience
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.TextReplacementConfig
+import net.kyori.adventure.text.format.TextColor
+import net.kyori.adventure.text.minimessage.MiniMessage
+import org.bukkit.entity.Player
+
+abstract class MainChatRenderer(
+ protected val plugin: Plugin,
+ protected val chatManager: ChatManager
+) : ChatRenderer {
+ protected val miniMessage = MiniMessage.miniMessage()
+ private val displayNameProvider = DisplayName(plugin)
+
+ protected abstract fun getChatFormat(): String
+
+ override fun render(source: Player, sourceDisplayName: Component, message: Component, viewer: Audience): Component {
+ val displayName = if (viewer is Player)
+ displayNameProvider.get(source, viewer)
+ else
+ displayNameProvider.get(source)
+
+ return miniMessage.deserialize(getChatFormat())
+ .replaceText(TextReplacementConfig.builder()
+ .match("").replacement(miniMessage.deserialize(displayName))
+ .build())
+ .replaceText(TextReplacementConfig.builder()
+ .match("").replacement(PastelMessage.survivalOf(
+ chatManager.processMessage(PastelMessage.deOf(message), source, viewer)))
+ .build())
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/action/CoinCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/action/CoinCommand.kt
new file mode 100644
index 0000000..634735e
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/action/CoinCommand.kt
@@ -0,0 +1,36 @@
+package com.otfhee.paper.commands.action
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.display.DisplayName
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import kotlin.random.Random
+
+class CoinCommand(private val plugin: Plugin) {
+ init {
+ CommandAPICommand("coin")
+ .withPermission("otfhee.commands.default.coin")
+ .executesPlayer(PlayerCommandExecutor { sender, _ ->
+ val messenger = Messenger(plugin)
+ val displayName = DisplayName(plugin)
+ val languageUtil = LanguageUtil(plugin)
+
+ val heads = {
+ plugin.server.onlinePlayers.forEach { player ->
+ messenger.info("${displayName.get(sender)} ${languageUtil.getMessage(player, "commands.coin.heads")}", player)
+ }
+ }
+ val tails = {
+ plugin.server.onlinePlayers.forEach { player ->
+ messenger.info("${displayName.get(sender)} ${languageUtil.getMessage(player, "commands.coin.tails")}", player)
+ }
+ }
+ val coins = arrayOf(heads, tails)
+ val randomIndex = Random.nextInt(coins.size)
+ coins[randomIndex].invoke()
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/action/DoCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/action/DoCommand.kt
new file mode 100644
index 0000000..9dc37cb
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/action/DoCommand.kt
@@ -0,0 +1,24 @@
+package com.otfhee.paper.commands.action
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.display.DisplayName
+import com.otfhee.paper.messenger.Messenger
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.GreedyStringArgument
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+
+class DoCommand(plugin: Plugin) {
+ init {
+ CommandAPICommand("do")
+ .withArguments(GreedyStringArgument("action"))
+ .withPermission("otfhee.commands.default.do")
+ .executesPlayer(PlayerCommandExecutor { sender, args ->
+ val action = args.get("action") as String
+ val messenger = Messenger(plugin)
+ val displayName = DisplayName(plugin)
+
+ messenger.info("*${displayName.get(sender)} $action *")
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/action/MeCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/action/MeCommand.kt
new file mode 100644
index 0000000..09bcbea
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/action/MeCommand.kt
@@ -0,0 +1,26 @@
+package com.otfhee.paper.commands.action
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.display.DisplayName
+import com.otfhee.paper.messenger.Messenger
+import dev.jorel.commandapi.CommandAPI
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.GreedyStringArgument
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+
+class MeCommand(plugin: Plugin) {
+ init {
+ CommandAPI.unregister("me")
+ CommandAPICommand("me")
+ .withArguments(GreedyStringArgument("action"))
+ .withPermission("otfhee.commands.default.me")
+ .executesPlayer(PlayerCommandExecutor { sender, args ->
+ val action = args.get("action") as String
+ val messenger = Messenger(plugin)
+ val displayName = DisplayName(plugin)
+
+ messenger.info("${displayName.get(sender)} $action *")
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/action/TryCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/action/TryCommand.kt
new file mode 100644
index 0000000..ddbded0
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/action/TryCommand.kt
@@ -0,0 +1,47 @@
+package com.otfhee.paper.commands.action
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.display.DisplayName
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.GreedyStringArgument
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import kotlin.random.Random
+
+class TryCommand(private val plugin: Plugin) {
+ init {
+ CommandAPICommand("try")
+ .withPermission("otfhee.commands.default.try")
+ .withArguments(GreedyStringArgument("action"))
+ .executesPlayer(PlayerCommandExecutor { sender, args ->
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+ val displayName = DisplayName(plugin)
+ val action = args.get("action") as String
+
+ val success = {
+ plugin.server.onlinePlayers.forEach { player ->
+ messenger.info(
+ "${displayName.get(sender)} ${
+ languageUtil.getMessage(player, "commands.try.success")
+ .replace("", action)
+ }", player)
+ }
+ }
+ val fail = {
+ plugin.server.onlinePlayers.forEach { player ->
+ messenger.info(
+ "${displayName.get(sender)} ${
+ languageUtil.getMessage(player, "commands.try.fail")
+ .replace("", action)
+ }", player)
+ }
+ }
+ val arr = arrayOf(success, fail)
+ val randomIndex = Random.nextInt(arr.size)
+ arr[randomIndex].invoke()
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/chat/GlobalModeCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/chat/GlobalModeCommand.kt
new file mode 100644
index 0000000..f349c4b
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/chat/GlobalModeCommand.kt
@@ -0,0 +1,31 @@
+package com.otfhee.paper.commands.chat
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.database.user.User
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class GlobalModeCommand(private val plugin: Plugin) {
+ init {
+ CommandAPICommand("global")
+ .withAliases("g")
+ .withPermission("otfhee.commands.default.global")
+ .executesPlayer(PlayerCommandExecutor { sender, _ ->
+ val languageUtil = LanguageUtil(plugin)
+
+ transaction {
+ val user = User[sender.uniqueId]
+ if (user.globalMode) {
+ user.globalMode = false
+ languageUtil.success("commands.global-mode.false", sender)
+ } else {
+ user.globalMode = true
+ languageUtil.success("commands.global-mode.true", sender)
+ }
+ }
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/chat/PrivateMessagesCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/chat/PrivateMessagesCommand.kt
new file mode 100644
index 0000000..506d4d3
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/chat/PrivateMessagesCommand.kt
@@ -0,0 +1,176 @@
+package com.otfhee.paper.commands.chat
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.chat.ChatManager
+import com.otfhee.paper.database.user.User
+import com.otfhee.paper.database.user.UserStatus
+import com.otfhee.paper.display.DisplayName
+import com.otfhee.paper.messenger.PastelMessage
+import com.otfhee.paper.messenger.PastelMiniMessage
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPI
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.GreedyStringArgument
+import dev.jorel.commandapi.arguments.PlayerArgument
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import net.kyori.adventure.key.Key
+import net.kyori.adventure.sound.Sound
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.TextReplacementConfig
+import net.kyori.adventure.text.minimessage.MiniMessage
+import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver
+import net.kyori.adventure.text.minimessage.tag.standard.StandardTags
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
+import org.bukkit.Bukkit
+import org.bukkit.entity.Player
+import org.jetbrains.exposed.sql.transactions.transaction
+import java.util.*
+import kotlin.collections.HashMap
+
+class PrivateMessagesCommand(
+ private val plugin: Plugin,
+ private val chatManager: ChatManager
+) {
+ private val lastMessengers = HashMap()
+ private val miniMessage = MiniMessage.miniMessage()
+ private val languageUtil = LanguageUtil(plugin)
+
+ init {
+ CommandAPI.unregister("msg")
+ CommandAPI.unregister("tell")
+ CommandAPI.unregister("w")
+
+ CommandAPICommand("message")
+ .withPermission("otfhee.commands.default.pm")
+ .withAliases("tell", "msg", "w")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(GreedyStringArgument("message"))
+ .executesPlayer(PlayerCommandExecutor{ player, args ->
+ val recipient = args.get("player") as Player
+ val message = args.get("message") as String
+
+ sendPrivateMessage(player, recipient, miniMessage.deserialize(message))
+ lastMessengers[recipient.uniqueId] = player.uniqueId
+ })
+ .register()
+
+ CommandAPICommand("reply")
+ .withAliases("r")
+ .withPermission("otfhee.commands.default.reply")
+ .withArguments(GreedyStringArgument("message"))
+ .executesPlayer(PlayerCommandExecutor{ player, args ->
+ val message = args.get("message") as String
+ val lastMessengerId = lastMessengers[player.uniqueId]
+
+ if (lastMessengerId == null) {
+ languageUtil.error("commands.private-messages.reply-error", player)
+ return@PlayerCommandExecutor
+ }
+
+ val recipient = Bukkit.getPlayer(lastMessengerId)
+ if (recipient == null || !recipient.isOnline) {
+ languageUtil.error("util.player-not-online", player)
+ return@PlayerCommandExecutor
+ }
+
+ sendPrivateMessage(player, recipient, miniMessage.deserialize(message))
+ lastMessengers[recipient.uniqueId] = player.uniqueId
+ })
+ .register()
+ }
+
+ private fun sendPrivateMessage(sender: Player, recipient: Player, message: Component) {
+ val inFormat = plugin.config.getString("chat.style.private-format.in")
+ ?: " → : "
+ val outFormat = plugin.config.getString("chat.style.private-format.out")
+ ?: " ← : "
+
+ val pastelMiniMessage = PastelMiniMessage.builder()
+ .tags(
+ TagResolver.builder()
+ .resolvers(
+ StandardTags.color(),
+ StandardTags.gradient(),
+ StandardTags.clickEvent(),
+ StandardTags.hoverEvent(),
+ StandardTags.decorations(),
+ StandardTags.reset(),
+ StandardTags.rainbow())
+ .build()
+ )
+ .pastelFactor(0.4)
+ .build()
+ val messageText = pastelMiniMessage.serialize(message)
+
+ val processedMessage = chatManager.processMessage(messageText, sender)
+
+ val miniMessage = MiniMessage.miniMessage()
+ val displayName = DisplayName(plugin)
+ val displaySender = displayName.get(sender)
+ val displayRecipient = displayName.get(recipient)
+
+ val inMessage = miniMessage.deserialize(inFormat)
+ .replaceText(
+ TextReplacementConfig.builder()
+ .match("").replacement(miniMessage.deserialize(displaySender))
+ .build())
+ .replaceText(
+ TextReplacementConfig.builder()
+ .match("").replacement(miniMessage.deserialize(displayRecipient))
+ .build())
+ .replaceText(
+ TextReplacementConfig.builder()
+ .match("").replacement(
+ if (sender.hasPermission("otfhee.premium.chat.formatting")) {
+ PastelMessage.survivalOf(processedMessage)
+ } else {
+ Component.text(processedMessage)
+ }
+ )
+ .build())
+
+ val outMessage = miniMessage.deserialize(outFormat)
+ .replaceText(
+ TextReplacementConfig.builder()
+ .match("").replacement(miniMessage.deserialize(displaySender))
+ .build())
+ .replaceText(
+ TextReplacementConfig.builder()
+ .match("").replacement(miniMessage.deserialize(displayRecipient))
+ .build())
+ .replaceText(
+ TextReplacementConfig.builder()
+ .match("").replacement(
+ if (sender.hasPermission("otfhee.premium.chat.formatting")) {
+ PastelMessage.survivalOf(processedMessage)
+ } else {
+ Component.text(processedMessage)
+ }
+ )
+ .build())
+
+ val dbSender =
+ transaction {
+ User[sender.uniqueId]
+ }
+ val dbRecipient =
+ transaction {
+ User[recipient.uniqueId]
+ }
+
+ if (dbSender.status != UserStatus.DO_NOT_DISTURB.name) {
+ sender.playSound(Sound.sound(Key.key("minecraft:entity.experience_orb.pickup"),
+ net.kyori.adventure.sound.Sound.Source.MASTER, 1f, 1f))
+ }
+ sender.sendMessage(inMessage)
+ if (dbRecipient.status != UserStatus.DO_NOT_DISTURB.name) {
+ recipient.playSound(Sound.sound(Key.key("minecraft:entity.player.levelup"),
+ net.kyori.adventure.sound.Sound.Source.MASTER, 1f, 1f))
+ }
+ recipient.sendMessage(outMessage)
+ }
+
+ fun cleanupPlayerData(player: Player) {
+ lastMessengers.remove(player.uniqueId)
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayColor.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayColor.kt
new file mode 100644
index 0000000..2b31520
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayColor.kt
@@ -0,0 +1,73 @@
+package com.otfhee.paper.commands.display
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.database.user.User
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.GreedyStringArgument
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import net.kyori.adventure.text.format.NamedTextColor
+import net.kyori.adventure.text.minimessage.MiniMessage
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class DisplayColor(private val plugin: Plugin) {
+ private val namedColors = NamedTextColor.NAMES.keys().joinToString("|")
+ private val hexColor = "#[0-9A-Fa-f]{6}"
+ private val anyColor = "$hexColor|$namedColors"
+
+ fun register(): CommandAPICommand {
+ return CommandAPICommand("color")
+ .withPermission("otfhee.commands.premium.display.color")
+ .withArguments(GreedyStringArgument("color"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val rawInput = args.get("color") as String
+ val languageUtil = LanguageUtil(plugin)
+
+ if (rawInput.length >= 255) {
+ languageUtil.error("commands.display.color.limit", player)
+ return@PlayerCommandExecutor
+ }
+
+ val fullTag = if (!isValidFormat(rawInput)) {
+ languageUtil.error("commands.display.color.format", player)
+ return@PlayerCommandExecutor
+ } else {
+ "<${rawInput}>"
+ }
+
+ try {
+ MiniMessage.miniMessage().deserialize(fullTag)
+ } catch (e: Exception) {
+ languageUtil.error("commands.display.color.format", player)
+ return@PlayerCommandExecutor
+ }
+
+ try {
+ transaction {
+ val user = User[player.uniqueId]
+ user.displayColor = fullTag
+ }
+ languageUtil.success("commands.display.color.success", player)
+ } catch (e: Exception) {
+ languageUtil.error("commands.display.color.error", player)
+ e.printStackTrace()
+ }
+ })
+ }
+
+ private fun isValidFormat(input: String): Boolean {
+ if (input.startsWith("color:")) {
+ val content = input.substringAfter("color:")
+ return content.matches(Regex(hexColor)) || content.matches(Regex(namedColors))
+ }
+
+ if (input.startsWith("gradient:")) {
+ val content = input.substringAfter("gradient:")
+ val colors = content.split(":")
+ return colors.size >= 2 &&
+ colors.all { it.matches(Regex(anyColor)) }
+ }
+
+ return false
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayCommand.kt
new file mode 100644
index 0000000..d2242eb
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayCommand.kt
@@ -0,0 +1,19 @@
+package com.otfhee.paper.commands.display
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.utils.PetUtil
+import dev.jorel.commandapi.CommandAPICommand
+import net.luckperms.api.LuckPerms
+
+class DisplayCommand(plugin: Plugin, luckPerms: LuckPerms, petUtil: PetUtil) {
+ init {
+ CommandAPICommand("display")
+ .withSubcommands(
+ DisplayColor(plugin).register(),
+ DisplayPrefix(plugin, luckPerms).register(),
+ DisplayPet(plugin, luckPerms, petUtil).register(),
+ DisplayText(plugin, luckPerms).register()
+ )
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayPet.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayPet.kt
new file mode 100644
index 0000000..f494482
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayPet.kt
@@ -0,0 +1,164 @@
+package com.otfhee.paper.commands.display
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.LanguageUtil
+import com.otfhee.paper.utils.PetUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.*
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import io.papermc.paper.math.Position
+import net.luckperms.api.LuckPerms
+import net.luckperms.api.model.user.User
+import org.bukkit.entity.Player
+import org.jetbrains.exposed.sql.transactions.transaction
+import java.util.UUID
+
+class DisplayPet(
+ private val plugin: Plugin,
+ private val luckPerms: LuckPerms,
+ private val petUtil: PetUtil
+) {
+ fun register(): CommandAPICommand {
+ return CommandAPICommand("pet")
+ .withSubcommand(CommandAPICommand("set")
+ .withArguments(petArgument())
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val pet = args.get("pet") as String
+ val user = luckPerms.userManager.loadUser(player.uniqueId)
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ plugin.server.regionScheduler.execute(plugin, player.location) {
+ petUtil.removePet(player)
+ }
+
+ if (!isValidPet(user.get(), pet)) {
+ languageUtil.error("commands.display.pet.set.permission", player)
+ return@PlayerCommandExecutor
+ }
+
+ transaction {
+ val db = com.otfhee.paper.database.user.User[player.uniqueId]
+
+ db.pet = pet.uppercase()
+ }
+
+ plugin.server.regionScheduler.execute(plugin, player.location) {
+ PetUtil.PetType.fromString(pet.uppercase())?.let {
+ petUtil.builder(
+ player,
+ it
+ )
+ .shoulderPosition(
+ PetUtil.ShoulderPosition.valueOf(
+ user.get().cachedData.metaData.getMetaValue("position")?.uppercase() ?: "RIGHT"
+ )
+ )
+ .build { pet, exception ->
+ messenger.success(
+ languageUtil.getMessage(player, "commands.display.pet.set.success")
+ .replace("", pet?.type?.name!!), player
+ )
+ if (exception != null) plugin.logger.warning("$exception")
+ }
+ }
+ }
+ }))
+ .withSubcommand(CommandAPICommand("hide")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ val languageUtil = LanguageUtil(plugin)
+
+ plugin.server.regionScheduler.execute(plugin, player.location) {
+ petUtil.removePet(player)
+ }
+ transaction {
+ val db = com.otfhee.paper.database.user.User[player.uniqueId]
+
+ db.pet = null
+ }
+
+ languageUtil.success("commands.pet.hide", player)
+ }))
+ .withSubcommand(CommandAPICommand("position")
+ .withArguments(StringArgument("position")
+ .replaceSuggestions(ArgumentSuggestions.strings("left", "right", "above"))
+ )
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val languageUtil = LanguageUtil(plugin)
+ val user = luckPerms.userManager.loadUser(player.uniqueId)
+ val position = args.get("position") as String
+ val db = transaction {
+ com.otfhee.paper.database.user.User[player.uniqueId]
+ }
+
+ when (position.lowercase()) {
+ "left" -> PetUtil.ShoulderPosition.LEFT
+ "right" -> PetUtil.ShoulderPosition.RIGHT
+ "above" -> PetUtil.ShoulderPosition.ABOVE
+ else -> {
+ languageUtil.error("commands.display.pet.position.error", player)
+ return@PlayerCommandExecutor
+ }
+ }
+
+ val meta = luckPerms.nodeBuilderRegistry
+ .forMeta()
+ .key("position")
+ .value(position)
+ .build()
+
+ user.get().data().clear { node ->
+ node.key.startsWith("meta.position.")
+ }
+
+ user.get().data().add(meta)
+
+ luckPerms.userManager.saveUser(user.get())
+
+ plugin.server.regionScheduler.execute(plugin, player.location) {
+ petUtil.removePet(player)
+ val pet = db.pet
+ if (pet != null) {
+ PetUtil.PetType.fromString(pet)?.let {
+ petUtil.builder(player, it)
+ .shoulderPosition(PetUtil.ShoulderPosition.valueOf(position.uppercase()))
+ .build { _, exception ->
+ languageUtil.success("commands.display.pet.position.success", player)
+ if (exception != null) plugin.logger.warning("$exception")
+ }
+ }
+ } else {
+ languageUtil.success("commands.display.pet.position.success", player)
+ return@execute
+ }
+ }
+ }))
+ }
+
+ private fun petArgument(): Argument? {
+ return GreedyStringArgument("pet")
+ .replaceSuggestions(ArgumentSuggestions.strings { info ->
+ val player = info.sender as? Player ?: return@strings emptyArray()
+ getAvailablePets(player.uniqueId).toTypedArray()
+ })
+ }
+
+ private fun isValidPet(user: User, pet: String): Boolean {
+ return getAvailablePets(user.uniqueId).contains(pet)
+ }
+
+ private fun getAvailablePets(uuid: UUID): List {
+ val user = luckPerms.userManager.loadUser(uuid)
+
+ return user.get().cachedData.permissionData.permissionMap
+ .entries
+ .filter { (key, value) ->
+ key.startsWith("pet.") && value
+ }
+ .map { (key, _) ->
+ key.substring(4)
+ }
+ .distinct()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayPrefix.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayPrefix.kt
new file mode 100644
index 0000000..9d7abf4
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayPrefix.kt
@@ -0,0 +1,99 @@
+package com.otfhee.paper.commands.display
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.Argument
+import dev.jorel.commandapi.arguments.ArgumentSuggestions
+import dev.jorel.commandapi.arguments.GreedyStringArgument
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import net.luckperms.api.LuckPerms
+import net.luckperms.api.model.user.User
+import net.luckperms.api.node.types.PrefixNode
+import org.bukkit.entity.Player
+import java.util.*
+
+class DisplayPrefix(private val plugin: Plugin, private val luckPerms: LuckPerms) {
+
+ fun register(): CommandAPICommand {
+ return CommandAPICommand("prefix")
+ .withSubcommand(CommandAPICommand("set")
+ .withArguments(prefixArgument())
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val prefix = args.get("prefix") as String
+ val user = luckPerms.userManager.loadUser(player.uniqueId)
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ if (!isValidPrefix(user.get(), prefix)) {
+ languageUtil.error("commands.display.prefix.set.permission", player)
+ return@PlayerCommandExecutor
+ }
+
+ val metaNode = luckPerms.nodeBuilderRegistry
+ .forPrefix()
+ .prefix(prefix)
+ .priority(100)
+ .build()
+
+ user.get().data().clear { node ->
+ node.key.startsWith("prefix.")
+ }
+
+ user.get().data().add(metaNode)
+
+ luckPerms.userManager.saveUser(user.get())
+
+ messenger.success(languageUtil.getMessage(player, "commands.display.prefix.set.success")
+ .replace("", prefix), player)
+ })
+ )
+ .withSubcommand(CommandAPICommand("hide")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ val user = luckPerms.userManager.loadUser(player.uniqueId)
+ val languageUtil = LanguageUtil(plugin)
+
+ val metaNode = luckPerms.nodeBuilderRegistry
+ .forPrefix()
+ .prefix("")
+ .priority(100)
+ .build()
+
+ user.get().data().clear { node ->
+ node.key.startsWith("prefix.")
+ }
+
+ user.get().data().add(metaNode)
+
+ luckPerms.userManager.saveUser(user.get())
+
+ languageUtil.success("commands.display.prefix.hide.success", player)
+ })
+ )
+
+ }
+
+ private fun prefixArgument(): Argument? {
+ return GreedyStringArgument("prefix")
+ .replaceSuggestions(ArgumentSuggestions.strings { info ->
+ val player = info.sender as? Player ?: return@strings emptyArray()
+ getAvailablePrefixes(player.uniqueId).toTypedArray()
+ })
+ }
+
+ private fun isValidPrefix(user: User, prefix: String): Boolean {
+ return getAvailablePrefixes(user.uniqueId).contains(prefix)
+ }
+
+ private fun getAvailablePrefixes(uuid: UUID): List {
+ val user = luckPerms.userManager.getUser(uuid) ?: return emptyList()
+ return user.getInheritedGroups(user.queryOptions)
+ .flatMap { group ->
+ group.nodes
+ .filterIsInstance()
+ .map { it.metaValue }
+ }
+ .distinct()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayText.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayText.kt
new file mode 100644
index 0000000..d28fbd5
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/display/DisplayText.kt
@@ -0,0 +1,150 @@
+package com.otfhee.paper.commands.display
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.database.user.User
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.ArgumentSuggestions
+import dev.jorel.commandapi.arguments.GreedyStringArgument
+import dev.jorel.commandapi.arguments.StringArgument
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import net.kyori.adventure.text.format.NamedTextColor
+import net.kyori.adventure.text.minimessage.MiniMessage
+import net.luckperms.api.LuckPerms
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class DisplayText(
+ private val plugin: Plugin,
+ private val luckPerms: LuckPerms
+) {
+ private val namedColors = NamedTextColor.NAMES.keys().joinToString("|")
+ private val hexColor = "#[0-9A-Fa-f]{6}"
+ private val anyColor = "$hexColor|$namedColors"
+
+ fun register(): CommandAPICommand {
+ return CommandAPICommand("text")
+ .withPermission("otfhee.commands.premium.display.text.color")
+ .withSubcommand(CommandAPICommand("color")
+ .withArguments(GreedyStringArgument("color"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val rawInput = args.get("color") as String
+ val languageUtil = LanguageUtil(plugin)
+
+ if (rawInput.length >= 255) {
+ languageUtil.error("commands.display.color.limit", player)
+ return@PlayerCommandExecutor
+ }
+
+ val fullTag = if (!isValidFormat(rawInput)) {
+ languageUtil.error("commands.display.color.format", player)
+ return@PlayerCommandExecutor
+ } else {
+ "<${rawInput}>"
+ }
+
+ try {
+ MiniMessage.miniMessage().deserialize(fullTag)
+ } catch (e: Exception) {
+ languageUtil.error("commands.display.color.format", player)
+ return@PlayerCommandExecutor
+ }
+
+ try {
+ val user = luckPerms.userManager.loadUser(player.uniqueId)
+
+ val meta = luckPerms.nodeBuilderRegistry
+ .forMeta()
+ .key("text.color")
+ .value(fullTag)
+ .build()
+
+ user.get().data().clear { node ->
+ node.key.startsWith("meta.text\\.color.")
+ }
+
+ user.get().data().add(meta)
+
+ luckPerms.userManager.saveUser(user.get())
+
+ languageUtil.success("commands.display.color.success", player)
+ } catch (e: Exception) {
+ languageUtil.error("commands.display.color.error", player)
+ e.printStackTrace()
+ }
+ })
+ )
+ .withSubcommand(
+ CommandAPICommand("decoration")
+ .withPermission("otfhee.commands.premium.display.text.decoration")
+ .withArguments(
+ StringArgument("decoration")
+ .replaceSuggestions(ArgumentSuggestions.strings("bold", "italic", "underlined", "strikethrough", "obfuscated"))
+ )
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val decoration = args.get("decoration") as String
+ val languageUtil = LanguageUtil(plugin)
+
+ val final = when(decoration) {
+ "bold" -> ""
+ "italic" -> ""
+ "underlined" -> ""
+ "strikethrough" -> ""
+ "obfuscated" -> ""
+ else -> ""
+ }
+
+ val user = luckPerms.userManager.loadUser(player.uniqueId)
+
+ val meta = luckPerms.nodeBuilderRegistry
+ .forMeta()
+ .key("text.decoration")
+ .value(final)
+ .build()
+
+ user.get().data().clear { node ->
+ node.key.startsWith("meta.text\\.decoration.")
+ }
+
+ user.get().data().add(meta)
+
+ luckPerms.userManager.saveUser(user.get())
+ languageUtil.success("commands.display.text.decoration.success", player)
+ })
+ )
+ .withSubcommand(CommandAPICommand("reset")
+ .withPermission("otfhee.commands.premium.display.text.reset")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ val user = luckPerms.userManager.loadUser(player.uniqueId)
+ val languageUtil = LanguageUtil(plugin)
+
+ user.get().data().clear { node ->
+ node.key.startsWith("meta.text\\.color.")
+ }
+
+ user.get().data().clear { node ->
+ node.key.startsWith("meta.text\\.decoration.")
+ }
+
+ luckPerms.userManager.saveUser(user.get())
+
+ languageUtil.success("commands.display.text.reset.success", player)
+ })
+ )
+ }
+
+ private fun isValidFormat(input: String): Boolean {
+ if (input.startsWith("color:")) {
+ val content = input.substringAfter("color:")
+ return content.matches(Regex(hexColor)) || content.matches(Regex(namedColors))
+ }
+
+ if (input.startsWith("gradient:")) {
+ val content = input.substringAfter("gradient:")
+ val colors = content.split(":")
+ return colors.size >= 2 &&
+ colors.all { it.matches(Regex(anyColor)) }
+ }
+
+ return false
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/security/BanCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/security/BanCommand.kt
new file mode 100644
index 0000000..fdf1226
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/security/BanCommand.kt
@@ -0,0 +1,107 @@
+package com.otfhee.paper.commands.security
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.utils.BanUtil
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPI
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.ArgumentSuggestions
+import dev.jorel.commandapi.arguments.GreedyStringArgument
+import dev.jorel.commandapi.arguments.OfflinePlayerArgument
+import dev.jorel.commandapi.arguments.StringArgument
+import dev.jorel.commandapi.executors.CommandExecutor
+import net.kyori.adventure.text.Component
+import org.bukkit.Bukkit
+import org.bukkit.OfflinePlayer
+import org.bukkit.entity.Player
+import java.util.regex.Pattern
+
+class BanCommand(private val plugin: Plugin) {
+ private val banUtil = BanUtil(plugin)
+ private val durationPattern = Pattern.compile("(\\d+)([dhms])")
+
+ init {
+ CommandAPI.unregister("ban")
+ CommandAPICommand("ban")
+ .withPermission("otfhee.commands.staff.ban")
+ .withArguments(OfflinePlayerArgument("player"))
+ .withArguments(
+ StringArgument("duration")
+ .replaceSuggestions(ArgumentSuggestions.strings("5s", "10m", "12h", "1d", "7d", "30d", "permanent")).setOptional(true)
+ )
+ .withArguments(GreedyStringArgument("reason").setOptional(true))
+ .executes(CommandExecutor { sender, args ->
+ val target = args.get("player") as OfflinePlayer
+ val durationArg = args.get("duration") as String? ?: "permanent"
+ val reason = args.get("reason") as String? ?: "Не указана"
+ val languageUtil = LanguageUtil(plugin)
+
+ if (target.isOnline && target.player?.hasPermission("otfhee.commands.ban.bypass") == true) {
+ languageUtil.error("commands.ban.cannot-ban", sender)
+ return@CommandExecutor
+ }
+
+ val duration = if (durationArg.equals("permanent", ignoreCase = true)) {
+ null
+ } else {
+ parseDuration(durationArg)
+ }
+
+ val adminUUID = if (sender is Player) sender.uniqueId else Bukkit.getOfflinePlayer("CONSOLE").uniqueId
+
+ val success = banUtil.banPlayer(
+ playerUUID = target.uniqueId,
+ reason = reason,
+ adminUUID = adminUUID,
+ durationMillis = duration
+ )
+
+ if (success) {
+ val banInfo = banUtil.getBanInfo(target.uniqueId)
+ if (banInfo != null) {
+ languageUtil.success("commands.ban.success", sender)
+ plugin.server.onlinePlayers.forEach { player ->
+ if (player.hasPermission("otfhee.util.ban.message")) {
+ banUtil.banMessage(banInfo, player)
+ }
+ }
+
+ if (target.isOnline && target.player != null) {
+ banUtil.kickBannedPlayer(target.player!!, reason, banInfo.expireDate)
+ }
+ } else {
+ languageUtil.success("commands.ban.success-fail", sender)
+ }
+ } else {
+ languageUtil.error("commands.ban.fail", sender)
+ }
+ })
+ .register()
+ }
+
+ private fun parseDuration(input: String): Long? {
+ try {
+ val matcher = durationPattern.matcher(input)
+ var totalMillis: Long = 0
+
+ while (matcher.find()) {
+ val amount = matcher.group(1).toLong()
+ val unit = matcher.group(2)
+
+ val millis = when (unit) {
+ "s" -> amount * 1000
+ "m" -> amount * 60 * 1000
+ "h" -> amount * 60 * 60 * 1000
+ "d" -> amount * 24 * 60 * 60 * 1000
+ else -> 0
+ }
+
+ totalMillis += millis
+ }
+
+ return if (totalMillis == 0L) null else totalMillis
+ } catch (e: Exception) {
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/security/MuteCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/security/MuteCommand.kt
new file mode 100644
index 0000000..be95714
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/security/MuteCommand.kt
@@ -0,0 +1,100 @@
+package com.otfhee.paper.commands.security
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.utils.LanguageUtil
+import com.otfhee.paper.utils.MuteUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.ArgumentSuggestions
+import dev.jorel.commandapi.arguments.OfflinePlayerArgument
+import dev.jorel.commandapi.arguments.StringArgument
+import dev.jorel.commandapi.executors.CommandExecutor
+import net.kyori.adventure.key.Key
+import net.kyori.adventure.sound.Sound
+import org.bukkit.OfflinePlayer
+import org.bukkit.entity.Player
+import java.util.regex.Pattern
+
+class MuteCommand(private val plugin: Plugin) {
+ private val durationPattern = Pattern.compile("(\\d+)([dhm])")
+ private val muteUtil = MuteUtil(plugin)
+
+ init {
+ CommandAPICommand("mute")
+ .withPermission("otfhee.commands.staff.mute")
+ .withArguments(OfflinePlayerArgument("player"))
+ .withArguments(
+ StringArgument("duration")
+ .replaceSuggestions(ArgumentSuggestions.strings("1m", "10m", "12h", "1d", "7d", "30d", "permanent")).setOptional(true)
+ )
+ .executes(CommandExecutor { sender, args ->
+ val player = args.get("player") as OfflinePlayer
+ val durationArg = args.get("duration") as String? ?: "permanent"
+ val languageUtil = LanguageUtil(plugin)
+
+ if (player.isOnline && player.player?.hasPermission("otfhee.commands.stuff.mute.bypass") == true) {
+ languageUtil.error("commands.mute.cannot", sender)
+ return@CommandExecutor
+ }
+
+ val duration = parseDurationMinutes(durationArg)
+
+ muteUtil.mutePlayer(
+ uuid = player.uniqueId,
+ durationMinutes = duration,
+ sender = sender
+ )
+
+ if (player.isOnline) {
+ player as Player
+ for (n in 1..100) {
+ player.playSound(
+ Sound.sound(
+ Key.key("minecraft:entity.enderman.death"),
+ Sound.Source.MASTER, 1f, 1f))
+ }
+ muteUtil.muteMessage(player)
+ }
+ })
+ .register()
+
+ CommandAPICommand("unmute")
+ .withPermission("otfhee.commands.staff.unmute")
+ .withArguments(OfflinePlayerArgument("player"))
+ .executes(CommandExecutor { sender, args ->
+ val player = args.get("player") as OfflinePlayer
+ val languageUtil = LanguageUtil(plugin)
+
+ if (muteUtil.unmute(player.uniqueId)) {
+ languageUtil.success("commands.unmute.success", sender)
+ } else {
+ languageUtil.error("commands.unmute.error", sender)
+ }
+ })
+ .register()
+ }
+
+ private fun parseDurationMinutes(input: String): Int? {
+ try {
+ val matcher = durationPattern.matcher(input)
+ var totalMinutes = 0
+
+ while (matcher.find()) {
+ val amount = matcher.group(1).toInt()
+ val unit = matcher.group(2)
+
+ val minutes = when (unit) {
+ "m" -> amount
+ "h" -> amount * 60
+ "d" -> amount * 60 * 24
+ else -> 0
+ }
+
+ totalMinutes += minutes
+ }
+
+ return if (totalMinutes == 0) null else totalMinutes
+ } catch (e: Exception) {
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/security/OpCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/security/OpCommand.kt
new file mode 100644
index 0000000..8330bdc
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/security/OpCommand.kt
@@ -0,0 +1,10 @@
+package com.otfhee.paper.commands.security
+
+import dev.jorel.commandapi.CommandAPI
+
+class OpCommand {
+ init {
+ CommandAPI.unregister("op")
+ CommandAPI.unregister("deop")
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/security/UnbanCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/security/UnbanCommand.kt
new file mode 100644
index 0000000..f8f4bcf
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/security/UnbanCommand.kt
@@ -0,0 +1,54 @@
+package com.otfhee.paper.commands.security
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.BanUtil
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPI
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.OfflinePlayerArgument
+import dev.jorel.commandapi.executors.CommandExecutor
+import org.bukkit.OfflinePlayer
+import org.bukkit.entity.Player
+
+class UnbanCommand(private val plugin: Plugin) {
+ private val banUtil = BanUtil(plugin)
+
+ init {
+ CommandAPI.unregister("pardon")
+ CommandAPICommand("unban")
+ .withPermission("otfhee.commands.staff.unban")
+ .withArguments(OfflinePlayerArgument("player"))
+ .executes(CommandExecutor { sender, args ->
+ val target = args.get("player") as OfflinePlayer
+ val playerName = target.name ?: "Unknown"
+ val banInfo = banUtil.getBanInfo(target.uniqueId)
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ if (banInfo == null) {
+ languageUtil.error("commands.unban.player-not-banned", sender)
+ return@CommandExecutor
+ }
+
+ val success = banUtil.unbanPlayer(target.uniqueId)
+
+ if (success) {
+ val adminName = if (sender is Player) sender.name else "CONSOLE"
+ languageUtil.success("commands.unban.success", sender)
+ plugin.server.onlinePlayers.forEach { player ->
+ if (player.hasPermission("otfhee.util.unban.message")) {
+ messenger.info(
+ languageUtil.getMessage(player, "util.unban.message")
+ .replace("", playerName)
+ .replace("", adminName), player
+ )
+ }
+ }
+ } else {
+ languageUtil.error("commands.unban.error", sender)
+ }
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/security/WarnCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/security/WarnCommand.kt
new file mode 100644
index 0000000..208690d
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/security/WarnCommand.kt
@@ -0,0 +1,231 @@
+package com.otfhee.paper.commands.security
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.database.ban.Ban
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.BanUtil
+import com.otfhee.paper.utils.LanguageUtil
+import com.otfhee.paper.utils.WarnUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.ArgumentSuggestions
+import dev.jorel.commandapi.arguments.GreedyStringArgument
+import dev.jorel.commandapi.arguments.OfflinePlayerArgument
+import dev.jorel.commandapi.arguments.StringArgument
+import dev.jorel.commandapi.executors.CommandExecutor
+import net.kyori.adventure.key.Key
+import net.kyori.adventure.sound.Sound
+import org.bukkit.Bukkit
+import org.bukkit.OfflinePlayer
+import org.bukkit.entity.Player
+import org.jetbrains.exposed.sql.transactions.transaction
+import java.util.regex.Pattern
+
+class WarnCommand(private val plugin: Plugin) {
+ private val warnUtil = WarnUtil(plugin)
+ private val durationPattern = Pattern.compile("(\\d+)([dhm])")
+
+ init {
+ CommandAPICommand("warn")
+ .withPermission("otfhee.commands.staff.warn")
+ .withArguments(OfflinePlayerArgument("player"))
+ .withArguments(
+ GreedyStringArgument("reason")
+ .replaceSuggestions(ArgumentSuggestions.strings(plugin.config.getConfigurationSection("warnings.reasons")?.getKeys(true)))
+ )
+ .executes(CommandExecutor { sender, args ->
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+ val target = args.get("player") as OfflinePlayer
+ val reason = args.get("reason") as String
+ val reasonsSection = plugin.config.getConfigurationSection("warnings.reasons")
+ val durationArg = if (reasonsSection != null && reasonsSection.contains(reason)) {
+ plugin.config.getString("warnings.reasons.${reason}") ?: "permanent"
+ } else if (sender.hasPermission("otfhee.commands.warn.reason")) {
+ "permanent"
+ } else {
+ languageUtil.error("commands.warn.reason-out", sender)
+ return@CommandExecutor
+ }
+
+ if (target.isOnline && target.player?.hasPermission("otfhee.commands.warn.bypass") == true) {
+ languageUtil.error("commands.warn.cannot-warn", sender)
+ return@CommandExecutor
+ }
+
+ val durationMinutes = if (durationArg.equals("permanent", ignoreCase = true)) {
+ null
+ } else {
+ parseDurationMinutes(durationArg)
+ }
+
+ val adminUUID = if (sender is Player) sender.uniqueId else Bukkit.getOfflinePlayer("CONSOLE").uniqueId
+
+ val warnInfo = warnUtil.warnPlayer(
+ playerUUID = target.uniqueId,
+ reason = reason,
+ adminUUID = adminUUID,
+ durationMinutes = durationMinutes
+ )
+
+ if (warnInfo != null) {
+ plugin.server.onlinePlayers.forEach { player ->
+ if (player.hasPermission("otfhee.util.warn.message")) {
+ warnUtil.warnMessage(warnInfo, player)
+ }
+ }
+
+ if (target.isOnline && target.player != null) {
+ target as Player
+ val warningCount = warnUtil.getActiveWarningsCount(target.uniqueId)
+ val message = languageUtil.getMessage(target.player!!, "util.warn.received")
+ .replace("", reason)
+ .replace("", warningCount.toString())
+ .replace("", "2")
+
+ for (n in 1..100) {
+ target.playSound(
+ Sound.sound(
+ Key.key("minecraft:entity.enderman.death"),
+ Sound.Source.MASTER, 1f, 1f))
+ }
+
+ messenger.warn(message, target.player!!)
+ }
+ languageUtil.success("commands.warn.success", sender)
+ } else {
+ languageUtil.success("commands.warn.third-warning", sender)
+ }
+ })
+ .register()
+
+ CommandAPICommand("warns")
+ .withPermission("otfhee.commands.staff.warnings")
+ .withArguments(OfflinePlayerArgument("player"))
+ .executes(CommandExecutor { sender, args ->
+ val target = args.get("player") as OfflinePlayer
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ val activeWarnings = warnUtil.getActiveWarnings(target.uniqueId)
+
+ if (activeWarnings.isEmpty()) {
+ languageUtil.info("commands.warnings.none", sender)
+ return@CommandExecutor
+ }
+
+ val playerName = target.name ?: target.uniqueId.toString()
+ messenger.info(languageUtil.getMessage(sender, "commands.warnings.header")
+ .replace("", playerName)
+ .replace("", activeWarnings.size.toString())
+ .replace("", "2"), sender)
+
+ activeWarnings.forEachIndexed { index, warn ->
+ val admin = if (Bukkit.getOfflinePlayer("CONSOLE").uniqueId == warn.adminUUID) {
+ "CONSOLE"
+ } else {
+ Bukkit.getOfflinePlayer(warn.adminUUID).name ?: warn.adminUUID.toString()
+ }
+ val date = java.time.LocalDateTime.of(
+ warn.warnDate.year, warn.warnDate.monthNumber, warn.warnDate.dayOfMonth,
+ warn.warnDate.hour, warn.warnDate.minute, warn.warnDate.second
+ ).format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy"))
+
+ val timeLeftInfo = if (warn.remainingTime != null && warn.remainingTime > 0) {
+ "истекает после ${warnUtil.formatTimeLeft(warn.remainingTime, sender)} игрового времени"
+ } else {
+ "навсегда"
+ }
+
+ val message = "#${index + 1} (ID: ${warn.id}): ${warn.reason} - От $admin в $date ($timeLeftInfo)"
+ messenger.info(message, sender)
+ }
+ })
+ .register()
+
+ CommandAPICommand("unwarn")
+ .withPermission("otfhee.commands.staff.unwarn")
+ .withArguments(OfflinePlayerArgument("player"))
+ .withArguments(StringArgument("warn_id"))
+ .executes(CommandExecutor { sender, args ->
+ val target = args.get("player") as OfflinePlayer
+ val warnIdStr = args.get("warn_id") as String
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ val warnId = warnIdStr.toIntOrNull()
+ if (warnId == null) {
+ languageUtil.error("commands.unwarn.invalid-id", sender)
+ return@CommandExecutor
+ }
+
+ val success = warnUtil.removeWarning(warnId)
+
+ if (success) {
+ languageUtil.success("commands.unwarn.success", sender)
+
+ if (target.isOnline && target.player != null) {
+ val message = languageUtil.getMessage(target.player!!, "util.unwarn.received")
+ messenger.info(message, target.player!!)
+ }
+ } else {
+ languageUtil.error("commands.unwarn.not-found", sender)
+ }
+ })
+ .register()
+
+ CommandAPICommand("unwarns")
+ .withPermission("otfhee.commands.staff.unwarns")
+ .withArguments(OfflinePlayerArgument("player"))
+ .executes(CommandExecutor { sender, args ->
+ val target = args.get("player") as OfflinePlayer
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ val count = warnUtil.clearWarnings(target.uniqueId)
+
+ if (count > 0) {
+ messenger.success(languageUtil.getMessage(sender, "commands.unwarns.success")
+ .replace("", count.toString()))
+
+ if (target.isOnline && target.player != null) {
+ val message = languageUtil.getMessage(target.player!!, "util.unwarns.received")
+ messenger.info(message, target.player!!)
+ }
+
+ transaction {
+ if (Ban.isActive(target.uniqueId)) {
+ BanUtil(plugin).unbanPlayer(target.uniqueId)
+ }
+ }
+ } else {
+ languageUtil.info("commands.unwarns.none", sender)
+ }
+ })
+ .register()
+ }
+
+ private fun parseDurationMinutes(input: String): Int? {
+ try {
+ val matcher = durationPattern.matcher(input)
+ var totalMinutes = 0
+
+ while (matcher.find()) {
+ val amount = matcher.group(1).toInt()
+ val unit = matcher.group(2)
+
+ val minutes = when (unit) {
+ "m" -> amount
+ "h" -> amount * 60
+ "d" -> amount * 60 * 24
+ else -> 0
+ }
+
+ totalMinutes += minutes
+ }
+
+ return if (totalMinutes == 0) null else totalMinutes
+ } catch (e: Exception) {
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/social/DiscordCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/social/DiscordCommand.kt
new file mode 100644
index 0000000..20b130e
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/social/DiscordCommand.kt
@@ -0,0 +1,25 @@
+package com.otfhee.paper.commands.social
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+
+class DiscordCommand(private val plugin: Plugin) {
+ init {
+ CommandAPICommand("discord")
+ .withAliases("ds")
+ .withPermission("otfhee.commands.default.discord")
+ .executesPlayer(PlayerCommandExecutor { sender, _ ->
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ messenger.info(
+ languageUtil.getMessage(sender, "commands.discord")
+ .replace("", plugin.config.getString("links.discord") ?: "https://otfhee.com/discord"), sender
+ )
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/social/RulesCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/social/RulesCommand.kt
new file mode 100644
index 0000000..b77d578
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/social/RulesCommand.kt
@@ -0,0 +1,24 @@
+package com.otfhee.paper.commands.social
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+
+class RulesCommand(private val plugin: Plugin) {
+ init {
+ CommandAPICommand("rules")
+ .withPermission("otfhee.commands.default.rules")
+ .executesPlayer(PlayerCommandExecutor { sender, _ ->
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ messenger.info(
+ languageUtil.getMessage(sender, "commands.rules")
+ .replace("", plugin.config.getString("links.rules") ?: "https://otfhee.com/rules"), sender
+ )
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/social/SiteCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/social/SiteCommand.kt
new file mode 100644
index 0000000..128aa4a
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/social/SiteCommand.kt
@@ -0,0 +1,24 @@
+package com.otfhee.paper.commands.social
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+
+class SiteCommand(private val plugin: Plugin) {
+ init {
+ CommandAPICommand("site")
+ .withPermission("otfhee.commands.default.site")
+ .executesPlayer(PlayerCommandExecutor { sender, _ ->
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ messenger.info(
+ languageUtil.getMessage(sender, "commands.site")
+ .replace("", plugin.config.getString("links.site") ?: "https://otfhee.com/"), sender
+ )
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/social/TelegramCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/social/TelegramCommand.kt
new file mode 100644
index 0000000..0b0e593
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/social/TelegramCommand.kt
@@ -0,0 +1,25 @@
+package com.otfhee.paper.commands.social
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+
+class TelegramCommand(private val plugin: Plugin) {
+ init {
+ CommandAPICommand("telegram")
+ .withAliases("tg")
+ .withPermission("otfhee.commands.default.telegram")
+ .executesPlayer(PlayerCommandExecutor { sender, _ ->
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ messenger.info(
+ languageUtil.getMessage(sender, "commands.telegram")
+ .replace("", plugin.config.getString("links.telegram") ?: "https://otfhee.com/telegram"), sender
+ )
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/util/LanguageCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/util/LanguageCommand.kt
new file mode 100644
index 0000000..4a574a2
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/util/LanguageCommand.kt
@@ -0,0 +1,34 @@
+package com.otfhee.paper.commands.util
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.database.user.User
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.ListArgumentBuilder
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class LanguageCommand(plugin: Plugin) {
+ private val languageUtil = LanguageUtil(plugin)
+
+ init {
+ CommandAPICommand("language")
+ .withAliases("lang")
+ .withPermission("otfhee.commands.default.language")
+ .withArguments(
+ ListArgumentBuilder("language")
+ .withList(languageUtil.getAvailableLanguages())
+ .withMapper { language -> language.lowercase() }
+ .buildGreedy()
+ )
+ .executesPlayer(PlayerCommandExecutor { sender, args ->
+ val language = args["language"] as List<*>
+ transaction {
+ val user = User[sender.uniqueId]
+ user.lang = language.stream().findAny().get().toString()
+ languageUtil.success("commands.lang.success", sender)
+ }
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/util/PingCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/util/PingCommand.kt
new file mode 100644
index 0000000..02497d5
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/util/PingCommand.kt
@@ -0,0 +1,26 @@
+package com.otfhee.paper.commands.util
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.telemetry.PingTelemetry
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+
+class PingCommand(private val plugin: Plugin) {
+ init {
+ CommandAPICommand("ping")
+ .withPermission("otfhee.commands.default.ping")
+ .executesPlayer(PlayerCommandExecutor { sender, _ ->
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+ val ping = PingTelemetry(sender, plugin)
+
+ messenger.info(
+ languageUtil.getMessage(sender, "commands.ping")
+ .replace("", ping.get()), sender
+ )
+ })
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/util/PremiumCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/util/PremiumCommand.kt
new file mode 100644
index 0000000..d1d6b3a
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/util/PremiumCommand.kt
@@ -0,0 +1,93 @@
+package com.otfhee.paper.commands.util
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.database.user.User
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.arguments.ArgumentSuggestions
+import dev.jorel.commandapi.arguments.OfflinePlayerArgument
+import dev.jorel.commandapi.arguments.StringArgument
+import dev.jorel.commandapi.executors.CommandExecutor
+import kotlinx.datetime.*
+import net.luckperms.api.LuckPerms
+import net.luckperms.api.node.Node
+import org.bukkit.OfflinePlayer
+import org.bukkit.entity.Player
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class PremiumCommand(private val plugin: Plugin, private val luckPerms: LuckPerms) {
+ init {
+ CommandAPICommand("premium")
+ .withPermission("otfhee.commands.staff.premium")
+ .withSubcommand(CommandAPICommand("give")
+ .withPermission("otfhee.commands.staff.premium")
+ .withArguments(OfflinePlayerArgument("player"))
+ .withArguments(
+ StringArgument("rate")
+ .replaceSuggestions(ArgumentSuggestions.strings("monthly", "quarterly", "half_year", "year"))
+ )
+ .executes(CommandExecutor {sender, args ->
+ val player = args.get("player") as OfflinePlayer
+ val rate = args.get("rate") as String
+ val languageUtil = LanguageUtil(plugin)
+ transaction {
+ if (!User.isExist(player.uniqueId)) {
+ User.create(player.uniqueId)
+ }
+ val user = User[player.uniqueId]
+ val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+
+ val date = when (rate) {
+ "monthly" -> now.date.plus(1, DateTimeUnit.MONTH)
+ "quarterly" -> now.date.plus(3, DateTimeUnit.MONTH)
+ "half_year" -> now.date.plus(6, DateTimeUnit.MONTH)
+ "year" -> now.date.plus(12, DateTimeUnit.MONTH)
+ else -> null
+ }
+ val luser = luckPerms.userManager.loadUser(player.uniqueId)
+
+ if (date != null) {
+ if (user.premiumDate != null) {
+ user.premiumDate =
+ LocalDateTime(user.premiumDate!!.date.plus(date.periodUntil(date)), now.time)
+ } else {
+ user.premiumDate = LocalDateTime(date, now.time)
+ }
+ luser.get().data().add(Node.builder("group.premium").build())
+ luckPerms.userManager.saveUser(luser.get())
+ languageUtil.success("commands.premium.give.success", sender)
+ if (player.isOnline) {
+ player as Player
+ languageUtil.info("commands.premium.give.notification", player)
+ }
+ } else {
+ languageUtil.error("commands.premium.give.rate.error", sender)
+ }
+ }
+ }))
+ .withSubcommand(CommandAPICommand("revoke")
+ .withArguments(OfflinePlayerArgument("player"))
+ .withPermission("otfhee.commands.staff.premium")
+ .executes(CommandExecutor {sender, args ->
+ val player = args.get("player") as OfflinePlayer
+ val languageUtil = LanguageUtil(plugin)
+ transaction {
+ if (!User.isExist(player.uniqueId)) {
+ languageUtil.error("util.database.not-found", sender)
+ }
+ val user = User[player.uniqueId]
+ val luser = luckPerms.userManager.loadUser(player.uniqueId)
+
+ if (user.premiumDate != null) {
+ user.premiumDate = null
+ luser.get().data().remove(Node.builder("group.premium").build())
+ luckPerms.userManager.saveUser(luser.get())
+ languageUtil.success("commands.premium.revoke.success", sender)
+ } else {
+ languageUtil.error("commands.premium.revoke.not-found", sender)
+ }
+ }
+ }))
+ .register()
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/commands/util/StatusCommand.kt b/paper/src/main/kotlin/com/otfhee/paper/commands/util/StatusCommand.kt
new file mode 100644
index 0000000..4bb1f63
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/commands/util/StatusCommand.kt
@@ -0,0 +1,50 @@
+package com.otfhee.paper.commands.util
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.database.user.User
+import com.otfhee.paper.database.user.UserStatus
+import com.otfhee.paper.messenger.Messenger
+import com.otfhee.paper.utils.LanguageUtil
+import dev.jorel.commandapi.CommandAPICommand
+import dev.jorel.commandapi.executors.PlayerCommandExecutor
+import org.bukkit.entity.Player
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class StatusCommand(private val plugin: Plugin) {
+ init {
+ CommandAPICommand("status")
+ .withPermission("otfhee.commands.default.status")
+ .withSubcommand(
+ CommandAPICommand("afk")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ setStatus(player, UserStatus.AFK)
+ })
+ )
+ .withSubcommand(
+ CommandAPICommand("dnd")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ setStatus(player, UserStatus.DO_NOT_DISTURB)
+ })
+ )
+ .withSubcommand(
+ CommandAPICommand("online")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ setStatus(player, UserStatus.ONLINE)
+ })
+ )
+ .register()
+ }
+
+ private fun setStatus(player: Player, status: UserStatus) {
+ transaction {
+ val user = User[player.uniqueId]
+ user.status = status.name
+ }
+ val languageUtil = LanguageUtil(plugin)
+ val messenger = Messenger(plugin)
+
+ messenger.success(
+ languageUtil.getMessage(player, "commands.status.success")
+ .replace("", status.name), player)
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/ban/Ban.kt b/paper/src/main/kotlin/com/otfhee/paper/database/ban/Ban.kt
new file mode 100644
index 0000000..0c1660e
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/ban/Ban.kt
@@ -0,0 +1,62 @@
+package com.otfhee.paper.database.ban
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import org.jetbrains.exposed.dao.UUIDEntity
+import org.jetbrains.exposed.dao.UUIDEntityClass
+import org.jetbrains.exposed.dao.id.EntityID
+import org.jetbrains.exposed.sql.transactions.transaction
+import java.util.*
+
+class Ban(id: EntityID) : UUIDEntity(id) {
+ companion object : UUIDEntityClass(Bans) {
+ fun isActive(uuid: UUID): Boolean {
+ return transaction {
+ val ban = Ban.findById(uuid) ?: return@transaction false
+
+ val expire = ban.expireDate
+ val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+
+ if (expire != null && expire < now) {
+ ban.delete()
+ return@transaction false
+ }
+
+ return@transaction true
+ }
+ }
+
+ fun create(
+ playerUUID: UUID,
+ reason: String,
+ adminUUID: UUID,
+ expireDate: LocalDateTime? = null
+ ): Ban? {
+ return transaction {
+ val existingBan = Ban.findById(playerUUID)
+ if (existingBan != null) {
+ val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+ if (existingBan.expireDate == null || existingBan.expireDate!! > now) {
+ return@transaction null
+ }
+
+ existingBan.delete()
+ }
+
+ Ban.new(playerUUID) {
+ this.reason = reason
+ this.adminUUID = adminUUID
+ this.banDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+ this.expireDate = expireDate
+ }
+ }
+ }
+ }
+
+ var reason by Bans.reason
+ var adminUUID by Bans.adminUUID
+ var banDate by Bans.banDate
+ var expireDate by Bans.expireDate
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/ban/BanInfo.kt b/paper/src/main/kotlin/com/otfhee/paper/database/ban/BanInfo.kt
new file mode 100644
index 0000000..3e5dbae
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/ban/BanInfo.kt
@@ -0,0 +1,24 @@
+package com.otfhee.paper.database.ban
+
+import kotlinx.datetime.LocalDateTime
+import java.util.*
+
+data class BanInfo(
+ val uuid: UUID,
+ val reason: String,
+ val adminUUID: UUID,
+ val banDate: LocalDateTime,
+ val expireDate: LocalDateTime?
+) {
+ companion object {
+ fun fromBan(ban: Ban): BanInfo {
+ return BanInfo(
+ uuid = ban.id.value,
+ reason = ban.reason,
+ adminUUID = ban.adminUUID,
+ banDate = ban.banDate,
+ expireDate = ban.expireDate
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/ban/Bans.kt b/paper/src/main/kotlin/com/otfhee/paper/database/ban/Bans.kt
new file mode 100644
index 0000000..2a33e3b
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/ban/Bans.kt
@@ -0,0 +1,11 @@
+package com.otfhee.paper.database.ban
+
+import org.jetbrains.exposed.dao.id.UUIDTable
+import org.jetbrains.exposed.sql.kotlin.datetime.datetime
+
+object Bans : UUIDTable("bans") {
+ val reason = text("reason")
+ val adminUUID = uuid("admin-uuid")
+ val banDate = datetime("ban-date")
+ val expireDate = datetime("expire-date").nullable()
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/mute/Mute.kt b/paper/src/main/kotlin/com/otfhee/paper/database/mute/Mute.kt
new file mode 100644
index 0000000..c1e333e
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/mute/Mute.kt
@@ -0,0 +1,67 @@
+package com.otfhee.paper.database.mute
+
+import com.otfhee.paper.database.warn.Warn
+import com.otfhee.paper.database.warn.Warns
+import kotlinx.datetime.Clock
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import org.jetbrains.exposed.dao.UUIDEntity
+import org.jetbrains.exposed.dao.UUIDEntityClass
+import org.jetbrains.exposed.dao.id.EntityID
+import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
+import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNotNull
+import org.jetbrains.exposed.sql.and
+import org.jetbrains.exposed.sql.transactions.transaction
+import java.util.*
+
+class Mute(id: EntityID) : UUIDEntity(id) {
+ companion object : UUIDEntityClass(Mutes) {
+ fun create(
+ uuid: UUID,
+ durationMinutes: Int? = null
+ ): Mute {
+ return transaction {
+ Mute.new(uuid) {
+ this.muteDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+ this.remainingTime = durationMinutes
+ }
+ }
+ }
+
+ fun unmute(uuid: UUID) : Boolean {
+ return transaction {
+ val user = Mute.findById(uuid) ?: return@transaction false
+ user.delete()
+ return@transaction true
+ }
+ }
+
+ fun isMute(uuid: UUID) : Boolean {
+ return transaction {
+ Mute.findById(uuid) != null
+ }
+ }
+
+ fun decreaseRemainingTime(uuid: UUID, minutesPlayed: Int) {
+ transaction {
+ Mute.find {
+ (Mutes.id eq uuid) and
+ (Mutes.remainingTime.isNotNull())
+ }.forEach { mute ->
+ val timeLeft = mute.remainingTime
+ if (timeLeft != null) {
+ val newTimeLeft = (timeLeft - minutesPlayed).coerceAtLeast(0)
+ mute.remainingTime = newTimeLeft
+
+ if (newTimeLeft <= 0) {
+ unmute(uuid = uuid)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ var muteDate by Mutes.muteDate
+ var remainingTime by Mutes.remainingTime
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/mute/Mutes.kt b/paper/src/main/kotlin/com/otfhee/paper/database/mute/Mutes.kt
new file mode 100644
index 0000000..b0fc1de
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/mute/Mutes.kt
@@ -0,0 +1,9 @@
+package com.otfhee.paper.database.mute
+
+import org.jetbrains.exposed.dao.id.UUIDTable
+import org.jetbrains.exposed.sql.kotlin.datetime.datetime
+
+object Mutes : UUIDTable("mutes") {
+ val muteDate = datetime("mute-date")
+ val remainingTime = integer("remaining-time").nullable()
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/user/User.kt b/paper/src/main/kotlin/com/otfhee/paper/database/user/User.kt
new file mode 100644
index 0000000..098567a
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/user/User.kt
@@ -0,0 +1,48 @@
+package com.otfhee.paper.database.user
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import org.jetbrains.exposed.dao.UUIDEntity
+import org.jetbrains.exposed.dao.UUIDEntityClass
+import org.jetbrains.exposed.dao.id.EntityID
+import org.jetbrains.exposed.sql.transactions.transaction
+import java.util.*
+
+class User(id: EntityID) : UUIDEntity(id) {
+ var firstJoin by Users.firstJoin
+ var lastJoin by Users.lastJoin
+ var globalMode by Users.globalMode
+ var displayColor by Users.displayColor
+ var guild by Users.guild
+ var premiumDate by Users.premiumDate
+ var status by Users.status
+ var lang by Users.lang
+ var pet by Users.pet
+
+ companion object : UUIDEntityClass(Users) {
+ fun isExist(uuid: UUID): Boolean {
+ return transaction {
+ User.findById(uuid) != null
+ }
+ }
+
+ fun create(uuid: UUID) {
+ val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+
+ transaction {
+ User.new(uuid) {
+ firstJoin = now
+ lastJoin = now
+ globalMode = false
+ displayColor = ""
+ guild = null
+ premiumDate = null
+ status = "ONLINE"
+ lang = "russian"
+ pet = null
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/user/UserStatus.kt b/paper/src/main/kotlin/com/otfhee/paper/database/user/UserStatus.kt
new file mode 100644
index 0000000..8e1150d
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/user/UserStatus.kt
@@ -0,0 +1,7 @@
+package com.otfhee.paper.database.user
+
+enum class UserStatus {
+ ONLINE,
+ AFK,
+ DO_NOT_DISTURB
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/user/Users.kt b/paper/src/main/kotlin/com/otfhee/paper/database/user/Users.kt
new file mode 100644
index 0000000..5313dbe
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/user/Users.kt
@@ -0,0 +1,16 @@
+package com.otfhee.paper.database.user
+
+import org.jetbrains.exposed.dao.id.UUIDTable
+import org.jetbrains.exposed.sql.kotlin.datetime.datetime
+
+object Users : UUIDTable("users") {
+ val firstJoin = datetime("first-join")
+ val lastJoin = datetime("last-join")
+ val premiumDate = datetime("premium-date").nullable()
+ val globalMode = bool("global-mode")
+ val displayColor = varchar("display-color", 255)
+ val guild = uuid("guild").nullable()
+ val status = varchar("status", 255)
+ val lang = varchar("lang", 255)
+ val pet = varchar("pet", 255).nullable()
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/warn/Warn.kt b/paper/src/main/kotlin/com/otfhee/paper/database/warn/Warn.kt
new file mode 100644
index 0000000..f25169d
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/warn/Warn.kt
@@ -0,0 +1,108 @@
+package com.otfhee.paper.database.warn
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import org.jetbrains.exposed.dao.IntEntity
+import org.jetbrains.exposed.dao.IntEntityClass
+import org.jetbrains.exposed.dao.id.EntityID
+import org.jetbrains.exposed.sql.SortOrder
+import org.jetbrains.exposed.sql.and
+import org.jetbrains.exposed.sql.transactions.transaction
+import java.util.*
+
+class Warn(id: EntityID) : IntEntity(id) {
+ companion object : IntEntityClass(Warns) {
+ fun create(
+ playerUUID: UUID,
+ reason: String,
+ adminUUID: UUID,
+ durationMinutes: Int? = null
+ ): Warn {
+ return transaction {
+ Warn.new {
+ this.playerUUID = playerUUID
+ this.reason = reason
+ this.adminUUID = adminUUID
+ this.warnDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+ this.remainingTime = durationMinutes
+ this.active = true
+ }
+ }
+ }
+
+ fun getActiveWarningsCount(playerUUID: UUID): Int {
+ return transaction {
+ Warn.find {
+ (Warns.playerUUID eq playerUUID) and (Warns.active eq true)
+ }.count().toInt()
+ }
+ }
+
+ fun getAllWarnings(playerUUID: UUID): List {
+ return transaction {
+ Warn.find { Warns.playerUUID eq playerUUID }
+ .orderBy(Warns.warnDate to SortOrder.DESC)
+ .map { WarnInfo.fromWarn(it) }
+ }
+ }
+
+ fun getActiveWarnings(playerUUID: UUID): List {
+ return transaction {
+ Warn.find {
+ (Warns.playerUUID eq playerUUID) and (Warns.active eq true)
+ }
+ .orderBy(Warns.warnDate to SortOrder.DESC)
+ .map { WarnInfo.fromWarn(it) }
+ }
+ }
+
+ fun decreaseRemainingTime(playerUUID: UUID, minutesPlayed: Int) {
+ transaction {
+ Warn.find {
+ (Warns.playerUUID eq playerUUID) and
+ (Warns.active eq true) and
+ (Warns.remainingTime.isNotNull())
+ }.forEach { warn ->
+ val timeLeft = warn.remainingTime
+ if (timeLeft != null) {
+ val newTimeLeft = (timeLeft - minutesPlayed).coerceAtLeast(0)
+ warn.remainingTime = newTimeLeft
+
+ if (newTimeLeft <= 0) {
+ warn.active = false
+ }
+ }
+ }
+ }
+ }
+
+ fun removeWarning(warnId: Int): Boolean {
+ return transaction {
+ val warn = Warn.findById(warnId) ?: return@transaction false
+ warn.active = false
+ return@transaction true
+ }
+ }
+
+ fun clearWarnings(playerUUID: UUID): Int {
+ return transaction {
+ var count = 0
+ Warn.find {
+ (Warns.playerUUID eq playerUUID) and (Warns.active eq true)
+ }.forEach {
+ it.active = false
+ count++
+ }
+ count
+ }
+ }
+ }
+
+ var playerUUID by Warns.playerUUID
+ var reason by Warns.reason
+ var adminUUID by Warns.adminUUID
+ var warnDate by Warns.warnDate
+ var remainingTime by Warns.remainingTime
+ var active by Warns.active
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/warn/WarnInfo.kt b/paper/src/main/kotlin/com/otfhee/paper/database/warn/WarnInfo.kt
new file mode 100644
index 0000000..934c8d5
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/warn/WarnInfo.kt
@@ -0,0 +1,28 @@
+package com.otfhee.paper.database.warn
+
+import kotlinx.datetime.LocalDateTime
+import java.util.*
+
+data class WarnInfo(
+ val id: Int,
+ val playerUUID: UUID,
+ val reason: String?,
+ val adminUUID: UUID,
+ val warnDate: LocalDateTime,
+ val remainingTime: Int?,
+ val active: Boolean
+) {
+ companion object {
+ fun fromWarn(warn: Warn): WarnInfo {
+ return WarnInfo(
+ id = warn.id.value,
+ playerUUID = warn.playerUUID,
+ reason = warn.reason,
+ adminUUID = warn.adminUUID,
+ warnDate = warn.warnDate,
+ remainingTime = warn.remainingTime,
+ active = warn.active
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/database/warn/Warns.kt b/paper/src/main/kotlin/com/otfhee/paper/database/warn/Warns.kt
new file mode 100644
index 0000000..92b72a2
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/database/warn/Warns.kt
@@ -0,0 +1,13 @@
+package com.otfhee.paper.database.warn
+
+import org.jetbrains.exposed.dao.id.IntIdTable
+import org.jetbrains.exposed.sql.kotlin.datetime.datetime
+
+object Warns : IntIdTable("warns") {
+ val playerUUID = uuid("player-uuid")
+ val reason = text("reason").nullable()
+ val adminUUID = uuid("admin-uuid")
+ val warnDate = datetime("warn-date")
+ val remainingTime = integer("remaining-time").nullable()
+ val active = bool("active").default(true)
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/display/Display.kt b/paper/src/main/kotlin/com/otfhee/paper/display/Display.kt
new file mode 100644
index 0000000..7a3db60
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/display/Display.kt
@@ -0,0 +1,8 @@
+package com.otfhee.paper.display
+
+import net.kyori.adventure.audience.Audience
+import org.bukkit.entity.Player
+
+interface Display {
+ fun get(player: Player, viewer: Audience? = null): String
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/display/DisplayName.kt b/paper/src/main/kotlin/com/otfhee/paper/display/DisplayName.kt
new file mode 100644
index 0000000..6b20527
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/display/DisplayName.kt
@@ -0,0 +1,50 @@
+package com.otfhee.paper.display
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.database.mute.Mutes.default
+import com.otfhee.paper.database.user.User
+import com.otfhee.paper.utils.LanguageUtil
+import net.kyori.adventure.audience.Audience
+import net.kyori.adventure.text.format.TextColor
+import net.kyori.adventure.text.minimessage.MiniMessage
+import net.luckperms.api.LuckPerms
+import org.bukkit.entity.Player
+import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class DisplayName(private val plugin: Plugin) : Display {
+ private val displayName = plugin.config.getString("display-name") ?: ""
+
+ override fun get(player: Player, viewer: Audience?): String {
+ val luckPerms: LuckPerms = plugin.luckPerms()
+ val user = luckPerms.userManager.getUser(player.uniqueId)!!
+ val db: User = transaction {
+ User[player.uniqueId]
+ }
+ val default = ""
+ val languageUtil = LanguageUtil(plugin)
+ val displayColor = if (player.hasPermission("otfhee.premium.display.color"))
+ db.displayColor
+ else
+ ""
+
+ val lpPrefix = user.cachedData.metaData.prefix
+ val miniMessage = MiniMessage.miniMessage()
+
+ val prefix = if (lpPrefix == "")
+ default
+ else if (lpPrefix != null)
+ miniMessage.serialize(miniMessage.deserialize("$lpPrefix ").color(TextColor.color(78, 92, 36)))
+ else
+ default
+
+ val display = displayName
+ .replace("", prefix)
+ .replace("", displayColor)
+ .replace("", "${languageUtil.getMessage(viewer ?: player, "chat.private-messages.display-name")}'>" +
+ "${player.name}")
+
+ return display
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/display/DisplayStatus.kt b/paper/src/main/kotlin/com/otfhee/paper/display/DisplayStatus.kt
new file mode 100644
index 0000000..60dc03d
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/display/DisplayStatus.kt
@@ -0,0 +1,22 @@
+package com.otfhee.paper.display
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.database.user.User
+import net.kyori.adventure.audience.Audience
+import org.bukkit.entity.Player
+import org.jetbrains.exposed.sql.transactions.transaction
+
+class DisplayStatus(private val plugin: Plugin) : Display {
+ override fun get(player: Player, viewer: Audience?): String {
+ val user = transaction {
+ User[player.uniqueId]
+ }
+
+ return when(user.status.lowercase()) {
+ "online" -> plugin.config.getString("icons.online").toString()
+ "afk" -> plugin.config.getString("icons.afk").toString()
+ "do_not_disturb" -> plugin.config.getString("icons.do_not_disturb").toString()
+ else -> "?"
+ }
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/display/DisplayWorld.kt b/paper/src/main/kotlin/com/otfhee/paper/display/DisplayWorld.kt
new file mode 100644
index 0000000..9ed0055
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/display/DisplayWorld.kt
@@ -0,0 +1,16 @@
+package com.otfhee.paper.display
+
+import com.otfhee.paper.Plugin
+import net.kyori.adventure.audience.Audience
+import org.bukkit.entity.Player
+
+class DisplayWorld(private val plugin: Plugin) : Display {
+ override fun get(player: Player, viewer: Audience?): String {
+ return when(player.world.name) {
+ "world" -> plugin.config.getString("icons.world").toString()
+ "world_nether" -> plugin.config.getString("icons.world_nether").toString()
+ "world_the_end" -> plugin.config.getString("icons.world_the_end").toString()
+ else -> "?"
+ }
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/listeners/AllayInteractListener.kt b/paper/src/main/kotlin/com/otfhee/paper/listeners/AllayInteractListener.kt
new file mode 100644
index 0000000..f4f6039
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/listeners/AllayInteractListener.kt
@@ -0,0 +1,19 @@
+package com.otfhee.paper.listeners
+
+import org.bukkit.entity.Allay
+import org.bukkit.event.EventHandler
+import org.bukkit.event.Listener
+import org.bukkit.event.player.PlayerInteractEntityEvent
+
+class AllayInteractListener : Listener {
+ @EventHandler
+ fun onPlayerInteractWithAllay(event: PlayerInteractEntityEvent) {
+ val metadata = event.rightClicked.getMetadata("interact")
+
+ val data = if (metadata.isNotEmpty()) metadata[0].value() as? Boolean ?: false else true
+
+ if (event.rightClicked is Allay && !data) {
+ event.isCancelled = true
+ }
+ }
+}
\ No newline at end of file
diff --git a/paper/src/main/kotlin/com/otfhee/paper/listeners/AntiGriefListener.kt b/paper/src/main/kotlin/com/otfhee/paper/listeners/AntiGriefListener.kt
new file mode 100644
index 0000000..ba70ec0
--- /dev/null
+++ b/paper/src/main/kotlin/com/otfhee/paper/listeners/AntiGriefListener.kt
@@ -0,0 +1,84 @@
+package com.otfhee.paper.listeners
+
+import com.otfhee.paper.Plugin
+import com.otfhee.paper.messenger.PastelMessage
+import com.otfhee.paper.utils.LanguageUtil
+import com.otfhee.paper.utils.TimeUnit
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.format.NamedTextColor
+import org.bukkit.Location
+import org.bukkit.Material
+import org.bukkit.Statistic
+import org.bukkit.entity.Player
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.block.BlockPlaceEvent
+
+class AntiGriefListener(private val plugin: Plugin) : Listener {
+ private val languageUtil: LanguageUtil = LanguageUtil(plugin)
+
+ private val spawnProtectionRadius: Int = plugin.config.getInt("anti-grief.spawn-protection-radius", 1500)
+
+ private val restrictedBlocks: Set = plugin.config.getStringList("anti-grief.restricted-blocks")
+ .mapNotNull { blockName ->
+ try {
+ Material.valueOf(blockName.uppercase())
+ } catch (e: IllegalArgumentException) {
+ plugin.logger.warning("Incorrect material in the configuration: $blockName")
+ null
+ }
+ }.toSet().ifEmpty {
+ setOf(
+ Material.TNT,
+ Material.FIRE,
+ Material.LAVA,
+ Material.LAVA_BUCKET,
+ Material.FLINT_AND_STEEL,
+ Material.END_CRYSTAL,
+ Material.RESPAWN_ANCHOR,
+ Material.WITHER_SKELETON_SKULL
+ )
+ }
+
+ @EventHandler(priority = EventPriority.HIGH)
+ fun onBlockPlace(event: BlockPlaceEvent) {
+ val player = event.player
+ val block = event.block
+
+ if (isInSpawnProtection(block.location) && restrictedBlocks.contains(block.type)) {
+ val playTime = getPlayerPlaytime(player)
+
+ if (playTime < 24 && !player.hasPermission("otfhee.staff.anti-grief.spawn")) {
+ event.isCancelled = true
+ val remainingTime = 24 - playTime
+ val message = languageUtil.getMessage(player, "anti-grief.spawn-protection")
+ .replace("