Portal module

This commit is contained in:
bizink
2025-06-12 00:11:46 +10:00
commit 5501b75d95
13 changed files with 768 additions and 0 deletions

119
.gitignore vendored Normal file
View File

@ -0,0 +1,119 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
.gradle
build/
# Ignore Gradle GUI config
gradle-app.setting
# Cache of project
.gradletasknamecache
**/build/
# Common working directory
run/
runs/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

50
build.gradle.kts Normal file
View File

@ -0,0 +1,50 @@
plugins {
kotlin("jvm") version "2.2.0-Beta2"
id("com.gradleup.shadow") version "8.3.0"
id("xyz.jpenilla.run-paper") version "2.3.1"
}
group = "com.pobnellion"
version = "2.0-SNAPSHOT"
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/") {
name = "papermc-repo"
}
maven("https://oss.sonatype.org/content/groups/public/") {
name = "sonatype"
}
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.5-R0.1-SNAPSHOT")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
tasks {
runServer {
// Configure the Minecraft version for our task.
// This is the only required configuration besides applying the plugin.
// Your plugin's jar (or shadowJar if present) will be used automatically.
minecraftVersion("1.21")
}
}
val targetJavaVersion = 21
kotlin {
jvmToolchain(targetJavaVersion)
}
tasks.build {
dependsOn("shadowJar")
}
tasks.processResources {
val props = mapOf("version" to version)
inputs.properties(props)
filteringCharset = "UTF-8"
filesMatching("plugin.yml") {
expand(props)
}
}

0
gradle.properties Normal file
View File

View File

@ -0,0 +1 @@
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip

1
settings.gradle.kts Normal file
View File

@ -0,0 +1 @@
rootProject.name = "pobutils"

View File

@ -0,0 +1,77 @@
package com.pobnellion.pobutils
import com.pobnellion.pobutils.modules.CmdModule
import com.pobnellion.pobutils.modules.ModuleBase
import com.pobnellion.pobutils.modules.portals.Portals
import org.bukkit.plugin.java.JavaPlugin
class Pobutils : JavaPlugin() {
companion object {
val availableModules = mutableMapOf<String, ModuleBase>()
val enabledModules : MutableMap<String, ModuleBase> = mutableMapOf()
fun getDisabledModuleNames() = availableModules.keys.filter { name -> !enabledModules.containsKey(name) }
fun isEnabled(moduleName: String) : Boolean = enabledModules.containsKey(moduleName)
}
override fun onEnable() {
loadDefaultConfig()
CmdModule.register(this)
registerModule(Portals(this))
logger.info("Registered ${availableModules.size} modules: [${availableModules.keys.joinToString()}]")
// Enable modules
for ((name, module) in availableModules) {
if (config.getBoolean("modules.${name}")) {
module.onEnable()
enabledModules[name] = module
}
}
logger.info("Loaded ${enabledModules.size} modules: [${enabledModules.keys.joinToString()}]")
}
override fun onDisable() {
// Plugin shutdown logic
}
private fun registerModule(module: ModuleBase) {
availableModules[module.name] = module
module.register()
}
private fun loadDefaultConfig() {
saveResource("config.yml", false)
config.addDefault("modules.noJoinMessage", false)
config.addDefault("modules.sit", false)
config.addDefault("modules.spawn", false)
config.addDefault("modules.portals", false)
config.addDefault("modules.warp", false)
config.addDefault("modules.hub", false)
config.addDefault("modules.disableTNT", false)
config.addDefault("modules.tabList", false)
config.addDefault("modules.formatChat", false)
config.addDefault("modules.disableTrample", false)
config.addDefault("modules.snowballDamage", false)
config.addDefault("data.spawn.location", "")
config.addDefault("data.spawn.spawnOnJoin", false)
config.addDefault("data.spawn.spawnOnDeath", false)
// config.addDefault("data.spawn.spawnOnFirstJoin", false);
config.addDefault("data.portals", "")
config.addDefault("data.warps", "")
config.addDefault("data.formatChat.serverAlias", "?")
config.addDefault("data.formatChat.messageFormat", "<gray><server_alias> <white><username>: <message>")
config.addDefault("data.formatChat.formatMessageText", true)
config.addDefault("settings.snowballDamage.damageExceptions", ArrayList<String?>())
config.addDefault("settings.snowballDamage.snowmenDontHitEachother", true)
config.options().copyDefaults(true)
saveConfig()
}
}

View File

@ -0,0 +1,96 @@
package com.pobnellion.pobutils.modules
import com.mojang.brigadier.Command
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.pobnellion.pobutils.Pobutils
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.TextComponent
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.plugin.java.JavaPlugin
@Suppress("UnstableApiUsage")
object CmdModule {
fun register(plugin: JavaPlugin) {
val command = Commands.literal("module")
.requires { source -> source.sender.hasPermission("pobutils.admin") }
.then(handleAction("enable", plugin))
.then(handleAction("disable", plugin))
.then(handleAction("reload", plugin))
.build()
plugin.lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) {commands ->
commands.registrar().register(command)
}
}
private fun handleAction(action: String, plugin: JavaPlugin) : LiteralArgumentBuilder<CommandSourceStack> {
return Commands.literal(action)
.then(Commands.argument("module", StringArgumentType.word())
.suggests { ctx, builder ->
when (action) {
"enable" -> Pobutils.getDisabledModuleNames().forEach { module -> builder.suggest(module) }
"disable" -> Pobutils.enabledModules.keys.forEach { module -> builder.suggest(module) }
"reload" -> Pobutils.enabledModules.keys.forEach { module -> builder.suggest(module) }
}
return@suggests builder.buildFuture()
}
.executes { ctx ->
val moduleName = StringArgumentType.getString(ctx, "module")
val module = Pobutils.availableModules[moduleName]
if (module == null) {
ctx.source.sender.sendMessage(Component.text("No module named '$moduleName'", NamedTextColor.RED))
return@executes Command.SINGLE_SUCCESS
}
val message : TextComponent
when (action) {
"enable" -> {
if (Pobutils.isEnabled(moduleName)) {
message = Component.text("Module '$moduleName' is already enabled", NamedTextColor.RED)
}
else {
Pobutils.enabledModules[moduleName] = module
module.onEnable()
plugin.config.set("modules.$moduleName", true)
plugin.saveConfig()
message = Component.text("Module '$moduleName' enabled", NamedTextColor.YELLOW)
}
}
"disable" -> {
if (!Pobutils.isEnabled(moduleName)) {
message = Component.text("Module '$moduleName' is already disabled", NamedTextColor.RED)
}
else {
Pobutils.enabledModules.remove(moduleName)
module.onDisable()
plugin.config.set("modules.$moduleName", false)
plugin.saveConfig()
message = Component.text("Module '$moduleName' disabled", NamedTextColor.YELLOW)
}
}
"reload" -> {
if (!Pobutils.isEnabled(moduleName)) {
message = Component.text("Module '$moduleName' is not currently enabled", NamedTextColor.RED)
}
else {
module.reload()
message = Component.text("Module '$moduleName' reloaded", NamedTextColor.YELLOW)
}
}
else -> message = Component.text("Unknown argument '$action'", NamedTextColor.RED)
}
plugin.server.onlinePlayers.forEach { player -> player.updateCommands() }
ctx.source.sender.sendMessage(message)
return@executes Command.SINGLE_SUCCESS
})
}
}

View File

@ -0,0 +1,11 @@
package com.pobnellion.pobutils.modules
import org.bukkit.plugin.java.JavaPlugin
abstract class ModuleBase(val plugin: JavaPlugin) {
abstract val name : String
abstract fun register()
abstract fun reload()
abstract fun onDisable()
abstract fun onEnable()
}

View File

@ -0,0 +1,115 @@
package com.pobnellion.pobutils.modules.portals
import com.mojang.brigadier.Command
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.pobnellion.pobutils.Pobutils
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
import io.papermc.paper.command.brigadier.argument.ArgumentTypes
import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.entity.Player
import org.bukkit.plugin.java.JavaPlugin
@Suppress("UnstableApiUsage")
object CmdPortal {
val serverList: MutableList<String> = mutableListOf()
fun register(plugin: JavaPlugin, portals: Portals) {
val command = Commands.literal("portal")
.requires { source ->
Pobutils.isEnabled("portals") &&
source.sender.hasPermission("pobutils.admin") &&
source.sender is Player
}
.then(addOrUpdatePortal("add", portals))
.then(addOrUpdatePortal("update", portals))
.then(removePortal(portals))
.then(listPortals(portals))
.build()
plugin.lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) { commands ->
commands.registrar().register(command)
}
}
private fun addOrUpdatePortal(action: String, portals: Portals) : LiteralArgumentBuilder<CommandSourceStack> {
var portalSubCommand = Commands.argument("name", StringArgumentType.word())
if (action == "update")
portalSubCommand = portalSubCommand.suggests { ctx, builder ->
portals.portals.forEach { portal -> builder.suggest(portal.name) }
return@suggests builder.buildFuture()
}
portalSubCommand.then(Commands.argument("location1", ArgumentTypes.blockPosition())
.then(Commands.argument("location2", ArgumentTypes.blockPosition())
.then(Commands.argument("destinationServer", StringArgumentType.word())
.suggests { ctx, builder ->
serverList.forEach { serverName -> builder.suggest(serverName) }
return@suggests builder.buildFuture()
}
.executes { ctx ->
val name = StringArgumentType.getString(ctx, "name")
val destServer = StringArgumentType.getString(ctx, "destinationServer")
val location1 = ctx.getArgument("location1", BlockPositionResolver::class.java).resolve(ctx.source)
val location2 = ctx.getArgument("location2", BlockPositionResolver::class.java).resolve(ctx.source)
if (!serverList.contains(destServer)) {
ctx.source.sender.sendMessage(Component.text("No server named $destServer", NamedTextColor.RED))
return@executes Command.SINGLE_SUCCESS
}
when (action) {
"add" if portals.exists(name) -> ctx.source.sender.sendMessage(Component.text("A portal called $name already exists!", NamedTextColor.RED))
"update" if !portals.exists(name) -> ctx.source.sender.sendMessage(Component.text("No portal named $name!", NamedTextColor.RED))
else -> {
portals.addOrUpdate(name, location1, location2, destServer)
val actionVerb = if (action == "add") "Added" else "Updated"
ctx.source.sender.sendMessage(Component.text("$actionVerb portal '$name'", NamedTextColor.YELLOW))
}
}
return@executes Command.SINGLE_SUCCESS
})))
return Commands
.literal(action)
.then(portalSubCommand)
}
private fun removePortal(portals: Portals) : LiteralArgumentBuilder<CommandSourceStack> {
return Commands.literal("remove")
.then(Commands.argument("portal", StringArgumentType.word())
.suggests { ctx, builder ->
portals.portals.forEach { portal -> builder.suggest(portal.name) }
return@suggests builder.buildFuture()
}
.executes { ctx ->
val name = StringArgumentType.getString(ctx, "portal")
if (!portals.exists(name))
ctx.source.sender.sendMessage(Component.text("No portal named $name!", NamedTextColor.RED))
else
portals.remove(name)
return@executes Command.SINGLE_SUCCESS
})
}
private fun listPortals(portals: Portals) : LiteralArgumentBuilder<CommandSourceStack> {
return Commands.literal("list")
.executes { ctx ->
ctx.source.sender.sendMessage(Component.text("There are ${portals.portals.count()} portals:", NamedTextColor.YELLOW))
portals.portals.forEach { portal ->
ctx.source.sender.sendMessage(Component.text(portal.toString(), NamedTextColor.YELLOW))
}
return@executes Command.SINGLE_SUCCESS
}
}
}

View File

@ -0,0 +1,58 @@
package com.pobnellion.pobutils.modules.portals
import org.bukkit.Location
import kotlin.math.max
import kotlin.math.min
class Portal {
val name: String
val destServer: String
private val x1: Int
private val y1: Int
private val z1: Int
private val x2: Int
private val y2: Int
private val z2: Int
constructor(name: String, destServer: String, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int) {
this.name = name
this.destServer = destServer
this.x1 = min(x1, x2)
this.y1 = min(y1, y2)
this.z1 = min(z1, z2)
this.x2 = max(x1, x2)
this.y2 = max(y1, y2)
this.z2 = max(z1, z2)
}
companion object {
fun deserialize(name: String, serializedData: String) : Portal {
val data = serializedData.split(" ")
return Portal(
name,
data[6],
data[0].toInt(),
data[1].toInt(),
data[2].toInt(),
data[3].toInt(),
data[4].toInt(),
data[5].toInt())
}
}
fun serialize(): String {
return "$x1 $y1 $z1 $x2 $y2 $z2 $destServer"
}
fun contains(location: Location) : Boolean {
return location.blockX >= x1 && location.blockX <= x2 &&
location.blockY >= y1 && location.blockY <= y2 &&
location.blockZ >= z1 && location.blockZ <= z2
}
override fun toString() : String {
return "[$name] ($x1 $y1 $z1) ($x2 $y2 $z2) dest: $destServer"
}
}

View File

@ -0,0 +1,164 @@
package com.pobnellion.pobutils.modules.portals
import com.google.common.io.ByteStreams
import com.pobnellion.pobutils.Pobutils
import com.pobnellion.pobutils.modules.ModuleBase
import io.papermc.paper.math.BlockPosition
import org.bukkit.Bukkit
import org.bukkit.Location
import org.bukkit.NamespacedKey
import org.bukkit.attribute.Attribute
import org.bukkit.attribute.AttributeModifier
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerMoveEvent
import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.plugin.messaging.PluginMessageListener
@Suppress("UnstableApiUsage")
class Portals(plugin: JavaPlugin) : ModuleBase(plugin), Listener, PluginMessageListener {
val portals: MutableList<Portal> = mutableListOf()
val portalCooldowns: MutableMap<Player, Location> = mutableMapOf()
override val name: String = "portals"
override fun register() {
Bukkit.getPluginManager().registerEvents(this, plugin)
plugin.server.pluginManager.registerEvents(this, plugin)
CmdPortal.register(plugin, this)
}
override fun reload() {
onDisable()
onEnable()
}
override fun onEnable() {
loadConfig()
plugin.server.messenger.registerOutgoingPluginChannel(plugin, "BungeeCord")
plugin.server.messenger.registerIncomingPluginChannel(plugin, "BungeeCord", this)
// use a player to trigger server list refresh (otherwise refresh will happen on player join)
if (plugin.server.onlinePlayers.count() > 0)
refreshServerList(plugin.server.onlinePlayers.first())
}
override fun onDisable() {
portals.clear()
CmdPortal.serverList.clear()
plugin.server.messenger.unregisterOutgoingPluginChannel(plugin)
plugin.server.messenger.unregisterIncomingPluginChannel(plugin)
}
private fun loadConfig() {
plugin.reloadConfig()
val config = plugin.config.getConfigurationSection("data.portals")
if (config != null)
portals.addAll(config.getKeys(false)
.map { name -> Portal.deserialize(name, config.get(name) as String) })
plugin.logger.info("Loaded ${portals.count()} portals")
}
private fun updateConfig() {
val portalsConfig = portals.associateBy(
{ portal -> portal.name },
{ portal -> portal.serialize() })
plugin.config.set("data.portals", portalsConfig)
plugin.saveConfig()
}
fun addOrUpdate(name: String, p1: BlockPosition, p2: BlockPosition, destServer: String) {
portals.removeIf { portal -> portal.name == name }
val portal = Portal(name, destServer, p1.blockX(), p1.blockY(), p1.blockZ(), p2.blockX(), p2.blockY(), p2.blockZ())
portals.add(portal)
updateConfig()
}
fun remove(name: String) {
if (!portals.removeIf { portal -> portal.name == name })
plugin.componentLogger.error("Failed to remove portal $name")
updateConfig()
}
fun exists(name: String?) : Boolean {
if (name == null)
return false
return portals.any { portal -> portal.name == name }
}
private fun refreshServerList(player: Player) {
plugin.logger.info("Refreshing server list")
val message = ByteStreams.newDataOutput()
message.writeUTF("GetServers")
player.sendPluginMessage(plugin, "BungeeCord", message.toByteArray())
}
private fun usePortal(event: PlayerMoveEvent, portal: Portal) {
val stopModifier = AttributeModifier(NamespacedKey.minecraft("stop"), -1.0, AttributeModifier.Operation.ADD_SCALAR)
event.player.getAttribute(Attribute.MOVEMENT_SPEED)!!.addTransientModifier(stopModifier)
event.player.getAttribute(Attribute.JUMP_STRENGTH)!!.addTransientModifier(stopModifier)
portalCooldowns[event.player] = event.from
val message = ByteStreams.newDataOutput()
message.writeUTF("Connect")
message.writeUTF(portal.destServer)
event.player.sendPluginMessage(plugin, "BungeeCord", message.toByteArray())
}
override fun onPluginMessageReceived(channel: String, player: Player, message: ByteArray) {
if (channel != "BungeeCord")
return
val message = ByteStreams.newDataInput(message)
val subchannel = message.readUTF()
if (subchannel == "GetServers") {
val serverList = message.readUTF()
CmdPortal.serverList.clear()
CmdPortal.serverList.addAll(serverList.split(", "))
}
}
// Switch player server when they enter a portal
@EventHandler
fun onPlayerMove(event: PlayerMoveEvent) {
if (!Pobutils.isEnabled(this.name))
return
if (portalCooldowns.containsKey(event.player))
return
for (portal in portals) {
if (portal.contains(event.to)) {
usePortal(event, portal)
break
}
}
}
// When they leave, reset player to where they were before they entered the portal
@EventHandler
fun onPlayerQuit(event: PlayerQuitEvent) {
if (portalCooldowns.containsKey(event.player)) {
event.player.teleport(portalCooldowns[event.player] as Location)
portalCooldowns.remove(event.player)
}
}
// Update server list used for command completion
// Plugin message requires a player to send the event so we wait for a player
@EventHandler
fun onPlayerJoin(event: PlayerJoinEvent) {
if (Pobutils.isEnabled(this.name) && CmdPortal.serverList.isEmpty())
plugin.server.scheduler.runTaskLaterAsynchronously(plugin, {_ -> refreshServerList(event.player) }, 10 )
}
}

View File

@ -0,0 +1,27 @@
modules:
noJoinMessage: true
sit: true
spawn: false
portals: false
warp: false
hub: true
disableTNT: false
tabList: false
formatChat: true
disableTrample: false
snowballDamage: false
data:
spawn:
location: ''
spawnOnJoin: false
spawnOnDeath: false
portals: ''
warps: ''
formatChat:
serverAlias: '?'
messageFormat: '<gray><server_alias> <white><username>: <message>'
formatMessageText: true
settings:
snowballDamage:
damageExceptions: []
snowmenDontHitEachother: true

View File

@ -0,0 +1,49 @@
name: pobutils
version: '2.0-SNAPSHOT'
main: com.pobnellion.pobutils.Pobutils
api-version: '1.21'
prefix: PobUtils
authors: [ Bizink ]
commands:
module:
description: configure a module
usage: /module <module> <enable|disable|config>
permission: pobutils.admin
spawn:
description: Teleport to spawn
usage: /spawn
permission: pobutils.user
setspawn:
description: Set spawn location
usage: /setspawn [location]
permission: pobutils.admin
sit:
description: Sit down for a while buddy
usage: /sit
permission: pobutils.user
portal:
description: add or remove a portal to another server
usage: /portal <add|update|remove> <name> [x1] [y1] [z1] [x2] [y2] [z2] [destinationServer]
permission: pobutils.admin
warp:
description: list or go to warps
usage: /warp [add|del|list|warpName] [warpName] [x] [y] [z] [yaw] [pitch]
permission: pobutils.user
hub:
description: go to hub server
usage: /hub
permission: pobutils.user
permissions:
pobutils.user:
default: true
pobutils.admin:
default: op