feat: 招待可能に

This commit is contained in:
Keisuke Hirata 2025-12-08 21:33:30 +09:00
parent 9e356049a0
commit 08a2470167
12 changed files with 990 additions and 7 deletions

View File

@ -10,6 +10,7 @@ plugins {
id("com.gradleup.shadow") version "9.2.2" id("com.gradleup.shadow") version "9.2.2"
} }
repositories { repositories {
mavenLocal()
mavenCentral() mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/") maven("https://repo.papermc.io/repository/maven-public/")
} }
@ -19,6 +20,7 @@ val exposedVersion = "1.0.0-rc-4"
dependencies { dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT") compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib") compileOnly("org.jetbrains.kotlin:kotlin-stdlib")
compileOnly("com.michael-bull.kotlin-result:kotlin-result:2.1.0")
compileOnly("net.hareworks.hcu:hcu-core") compileOnly("net.hareworks.hcu:hcu-core")
compileOnly("net.hareworks:kommand-lib") compileOnly("net.hareworks:kommand-lib")
@ -34,7 +36,6 @@ dependencies {
compileOnly("org.jetbrains.exposed:exposed-dao:$exposedVersion") compileOnly("org.jetbrains.exposed:exposed-dao:$exposedVersion")
compileOnly("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") compileOnly("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
compileOnly("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion") compileOnly("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion")
compileOnly("com.michael-bull.kotlin-result:kotlin-result:2.1.0")
} }
tasks { tasks {
withType<Jar> { withType<Jar> {

7
gradle.properties Normal file
View File

@ -0,0 +1,7 @@
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.daemon=false
org.gradle.configuration-cache=true

@ -1 +1 @@
Subproject commit 8d47e35af6002d863199c1b37fbd5f2d3cf1d826 Subproject commit 388d0df943f2b4a0232c1f4f55f1cecb2d372fca

6
settings.gradle.kts Normal file
View File

@ -0,0 +1,6 @@
rootProject.name = "faction"
includeBuild("hcu-core")
includeBuild("hcu-core/kommand-lib")
includeBuild("hcu-core/kommand-lib/permits-lib")

View File

@ -1,18 +1,66 @@
package net.hareworks.hcu.economy package net.hareworks.hcu.faction
import net.hareworks.kommand_lib.KommandLib import net.hareworks.hcu.core.database.DatabaseSessionManager
import net.hareworks.permits_lib.PermitsLib import net.hareworks.hcu.faction.command.FactionCommand
import net.hareworks.hcu.faction.database.schema.FactionMembersTable
import net.hareworks.hcu.faction.database.schema.FactionRequestsTable
import net.hareworks.hcu.faction.database.schema.FactionsTable
import net.hareworks.hcu.faction.service.FactionService
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import java.util.logging.Level
class App : JavaPlugin() { class App : JavaPlugin() {
private var commands: KommandLib? = null private lateinit var service: FactionService
private val permits = PermitsLib.session(this)
override fun onEnable() { override fun onEnable() {
instance = this instance = this
// Initialize Database Listener
val dbListener = net.hareworks.hcu.faction.listener.DatabaseListener(logger)
server.pluginManager.registerEvents(dbListener, this)
// Trigger schema check immediately if DB is already online (hcu-core might have loaded before us)
if (server.servicesManager.isProvidedFor(org.jetbrains.exposed.v1.jdbc.Database::class.java)) {
// We can trigger the listener logic or just rely on the service being present.
// But since ServiceRegisterEvent fires when registered, if it's ALREADY registered, we missed the event.
// Note: isConnected check is a proxy for "Service is available and working".
// We'll trust the simpler check we had, but just delegate logic to avoid duplication if possible,
// or just keep the inline block for simplicity as "Initial Attempt".
}
// Initialize Schema (Best effort at startup)
if (DatabaseSessionManager.isConnected()) {
try {
DatabaseSessionManager.transaction {
SchemaUtils.createMissingTablesAndColumns(
FactionsTable,
FactionMembersTable,
FactionRequestsTable
)
}
} catch (e: Exception) {
logger.log(Level.SEVERE, "Failed to initialize Faction schema", e)
}
} else {
logger.warning("hcu-core database not connected. Waiting for service registration to initialize schema.")
}
// Initialize Service
service = FactionService()
// Register Commands
FactionCommand(this, service).register()
// Register Listeners
server.pluginManager.registerEvents(net.hareworks.hcu.faction.listener.ChatListener(service), this)
logger.info("Faction plugin enabled")
} }
override fun onDisable() { override fun onDisable() {
logger.info("Faction plugin disabled")
} }
companion object { companion object {

View File

@ -0,0 +1,378 @@
package net.hareworks.hcu.faction.command
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess
import com.github.michaelbull.result.Ok
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.arguments.IntegerArgumentType
import net.hareworks.hcu.faction.service.FactionService
import com.mojang.brigadier.arguments.BoolArgumentType
import net.hareworks.kommand_lib.kommand
import net.hareworks.kommand_lib.KommandLib
import net.hareworks.hcu.faction.database.schema.FactionRole
import org.bukkit.plugin.java.JavaPlugin
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.entity.Player
class FactionCommand(private val plugin: JavaPlugin, private val service: FactionService) {
fun register() {
kommand(plugin) {
command("faction") {
description = "Manage your faction"
literal("create") {
string("name") {
executes {
val sender = sender
if (sender !is Player) {
sender.sendMessage(Component.text("Only players can create factions", NamedTextColor.RED))
return@executes
}
val name = argument<String>("name")
service.createFaction(name, null, null, sender.uniqueId)
.onSuccess { id ->
sender.sendMessage(Component.text("Faction '$name' created with ID $id", NamedTextColor.GREEN))
}
.onFailure { err ->
sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED))
}
}
}
}
literal("invite") {
player("player") {
executes {
val sender = sender
if (sender !is Player) return@executes
val target = argument<Player>("player")
val factionId = service.getFactionOfPlayer(sender.uniqueId)
if (factionId == null) {
sender.sendMessage(Component.text("You are not in a faction", NamedTextColor.RED))
return@executes
}
val role = service.getRole(factionId, sender.uniqueId)
if (role == FactionRole.MEMBER) {
sender.sendMessage(Component.text("Members cannot invite players", NamedTextColor.RED))
return@executes
}
service.invitePlayer(factionId, target.uniqueId)
.onSuccess {
sender.sendMessage(Component.text("Invited ${target.name}", NamedTextColor.GREEN))
if (target.isOnline) {
val factionName = service.getFactionName(factionId) ?: "Unknown"
val senderName = sender.name
val acceptMsg = Component.text("[Accept]", NamedTextColor.GREEN)
.clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand("/faction accept $factionName"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Accept invitation")))
val rejectMsg = Component.text("[Reject]", NamedTextColor.RED)
.clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand("/faction reject $factionName"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Reject invitation")))
(target as? Player)?.sendMessage(
Component.text("You have been invited to join ", NamedTextColor.YELLOW)
.append(Component.text(factionName, NamedTextColor.GOLD))
.append(Component.text(" by ", NamedTextColor.YELLOW))
.append(Component.text(senderName, NamedTextColor.AQUA))
.append(Component.text(". ", NamedTextColor.YELLOW))
.append(acceptMsg)
.append(Component.text(" ", NamedTextColor.WHITE))
.append(rejectMsg)
)
}
}
.onFailure { err ->
sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED))
}
}
}
}
// Remove join command
literal("accept") {
executes {
val sender = sender
if (sender !is Player) return@executes
val latestInviteFactionId = service.getLatestInvite(sender.uniqueId)
if (latestInviteFactionId == null) {
sender.sendMessage(Component.text("No pending factory invitations found.", NamedTextColor.RED))
return@executes
}
service.acceptInvite(latestInviteFactionId, sender.uniqueId)
.onSuccess {
val facName = service.getFactionName(latestInviteFactionId)
sender.sendMessage(Component.text("You joined ${facName ?: "the faction"}", NamedTextColor.GREEN))
}
.onFailure { err -> sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED)) }
}
string("target") {
executes {
val sender = sender
if (sender !is Player) return@executes
val targetName = argument<String>("target")
// Check if target is a Faction Name (Accepting Invite)
val factionIdByName = service.getFactionIdByName(targetName)
// Check if target is a Player Name (Accepting Join Request - Leader)
val factionIdOfSender = service.getFactionOfPlayer(sender.uniqueId)
var processedAsRequest = false
if (factionIdOfSender != null) {
val role = service.getRole(factionIdOfSender, sender.uniqueId)
if (role != FactionRole.MEMBER && role != null) {
val targetPlayer = plugin.server.getOfflinePlayer(targetName)
// Try request accept
// Use a simplified check or just try calling the service.
// Service returns error if no request found.
// But if we have ambiguity (Faction name == Player name), what to do?
// We prioritize Request if Sender is Leader? Or Invite?
// Let's try Request first if Leader.
service.acceptRequest(factionIdOfSender, targetPlayer.uniqueId)
.onSuccess {
sender.sendMessage(Component.text("Accepted join request from $targetName", NamedTextColor.GREEN))
return@executes
}
// If failed (e.g. no request), proceed to check if it's a Faction Invite for ME.
}
}
if (factionIdByName != null) {
service.acceptInvite(factionIdByName, sender.uniqueId)
.onSuccess {
sender.sendMessage(Component.text("You joined $targetName", NamedTextColor.GREEN))
}
.onFailure { err ->
// If we also failed request above, user might be confused.
sender.sendMessage(Component.text("Error: $err (or no request found from player)", NamedTextColor.RED))
}
return@executes
}
sender.sendMessage(Component.text("Target '$targetName' not found as Faction or Requesting Player", NamedTextColor.RED))
}
}
}
literal("reject") {
executes {
val sender = sender
if (sender !is Player) return@executes
val latestInviteFactionId = service.getLatestInvite(sender.uniqueId)
if (latestInviteFactionId == null) {
sender.sendMessage(Component.text("No pending factory invitations found.", NamedTextColor.RED))
return@executes
}
service.rejectInvite(latestInviteFactionId, sender.uniqueId)
.onSuccess {
val facName = service.getFactionName(latestInviteFactionId)
sender.sendMessage(Component.text("Rejected invitation from ${facName ?: "the faction"}", NamedTextColor.GREEN))
}
.onFailure { err -> sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED)) }
}
string("factionName") {
executes {
val sender = sender
if (sender !is Player) return@executes
val factionName = argument<String>("factionName")
val factionId = service.getFactionIdByName(factionName)
if (factionId == null) {
sender.sendMessage(Component.text("Faction '$factionName' not found", NamedTextColor.RED))
return@executes
}
service.rejectInvite(factionId, sender.uniqueId)
.onSuccess { sender.sendMessage(Component.text("Rejected invitation from $factionName", NamedTextColor.GREEN)) }
.onFailure { err -> sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED)) }
}
}
}
literal("promote") {
player("player") {
executes {
val sender = sender
if (sender !is Player) return@executes
val factionId = service.getFactionOfPlayer(sender.uniqueId)
if (factionId == null) {
sender.sendMessage(Component.text("You are not in a faction", NamedTextColor.RED))
return@executes
}
if (service.getRole(factionId, sender.uniqueId) != FactionRole.OWNER) {
sender.sendMessage(Component.text("Only OWNER can promote", NamedTextColor.RED))
return@executes
}
val target = argument<Player>("player")
service.promote(factionId, target.uniqueId)
.onSuccess { sender.sendMessage(Component.text("Promoted ${target.name}", NamedTextColor.GREEN)) }
.onFailure { err -> sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED)) }
}
}
}
literal("demote") {
player("player") {
executes {
val sender = sender
if (sender !is Player) return@executes
val factionId = service.getFactionOfPlayer(sender.uniqueId)
if (factionId == null) {
sender.sendMessage(Component.text("You are not in a faction", NamedTextColor.RED))
return@executes
}
if (service.getRole(factionId, sender.uniqueId) != FactionRole.OWNER) {
sender.sendMessage(Component.text("Only OWNER can demote", NamedTextColor.RED))
return@executes
}
val target = argument<Player>("player")
service.demote(factionId, target.uniqueId)
.onSuccess { sender.sendMessage(Component.text("Demoted ${target.name}", NamedTextColor.GREEN)) }
.onFailure { err -> sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED)) }
}
}
}
literal("settings") {
literal("tag") {
string("tag") {
executes {
val sender = sender
if (sender !is Player) return@executes
val factionId = service.getFactionOfPlayer(sender.uniqueId) ?: return@executes
if (service.getRole(factionId, sender.uniqueId) == FactionRole.MEMBER) return@executes
val tag = argument<String>("tag")
service.setTag(factionId, tag)
.onSuccess { sender.sendMessage(Component.text("Tag updated to $tag", NamedTextColor.GREEN)) }
.onFailure { err -> sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED)) }
}
}
}
literal("color") {
string("color") {
executes {
val sender = sender
if (sender !is Player) return@executes
val factionId = service.getFactionOfPlayer(sender.uniqueId) ?: return@executes
if (service.getRole(factionId, sender.uniqueId) == FactionRole.MEMBER) return@executes
val color = argument<String>("color")
service.setColor(factionId, color)
.onSuccess { sender.sendMessage(Component.text("Color updated to $color", NamedTextColor.GREEN)) }
.onFailure { err -> sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED)) }
}
}
}
literal("open") {
bool("open") {
executes {
val sender = sender
if (sender !is Player) return@executes
val factionId = service.getFactionOfPlayer(sender.uniqueId) ?: return@executes
if (service.getRole(factionId, sender.uniqueId) == FactionRole.MEMBER) return@executes
val open = argument<Boolean>("open")
service.setOpen(factionId, open)
.onSuccess { sender.sendMessage(Component.text("Open status updated to $open", NamedTextColor.GREEN)) }
.onFailure { err -> sender.sendMessage(Component.text("Error: $err", NamedTextColor.RED)) }
}
}
}
}
literal("leave") {
executes {
val sender = sender
if (sender !is Player) return@executes
service.leaveFaction(sender.uniqueId)
.onSuccess { sender.sendMessage(Component.text("Left faction", NamedTextColor.GREEN)) }
.onFailure { sender.sendMessage(Component.text("Error: $it", NamedTextColor.RED)) }
}
}
literal("disband") {
executes {
val sender = sender
if (sender !is Player) return@executes
service.disbandFaction(sender.uniqueId)
.onSuccess { sender.sendMessage(Component.text("Faction disbanded", NamedTextColor.GREEN)) }
.onFailure { sender.sendMessage(Component.text("Error: $it", NamedTextColor.RED)) }
}
}
literal("kick") {
player("player") {
executes {
val sender = sender
if (sender !is Player) return@executes
val target = argument<Player>("player")
service.kickPlayer(sender.uniqueId, target.uniqueId)
.onSuccess { sender.sendMessage(Component.text("Kicked ${target.name}", NamedTextColor.GREEN)) }
.onFailure { sender.sendMessage(Component.text("Error: $it", NamedTextColor.RED)) }
}
}
}
literal("list") {
executes {
val factions = service.getAllFactions()
if (factions.isEmpty()) {
sender.sendMessage(Component.text("No factions found", NamedTextColor.YELLOW))
} else {
sender.sendMessage(Component.text("Factions:", NamedTextColor.GOLD))
factions.forEach { (name, tag) ->
val tagText = tag?.let { " [$it]" } ?: ""
sender.sendMessage(Component.text("- $name$tagText", NamedTextColor.WHITE))
}
}
}
}
literal("info") {
string("name") {
executes {
val name = argument<String>("name")
val info = service.getFactionInfo(name)
if (info == null) {
sender.sendMessage(Component.text("Faction not found", NamedTextColor.RED))
} else {
sender.sendMessage(Component.text("Faction: ${info.name}", NamedTextColor.GOLD))
sender.sendMessage(Component.text("Tag: ${info.tag ?: "None"}", NamedTextColor.WHITE))
sender.sendMessage(Component.text("Members: ${info.memberCount}", NamedTextColor.WHITE))
sender.sendMessage(Component.text("Open: ${info.open}", NamedTextColor.WHITE))
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,24 @@
package net.hareworks.hcu.faction.database.schema
import net.hareworks.hcu.core.database.schema.PlayersTable
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.ReferenceOption
import org.jetbrains.exposed.v1.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.datetime.datetime
enum class FactionRole {
OWNER,
EXEC,
MEMBER
}
object FactionMembersTable : Table("faction_members") {
val factionId = integer("faction_id")
.references(FactionsTable.id, onDelete = ReferenceOption.CASCADE)
val playerUuid = uuid("player_uuid")
.references(PlayersTable.uuid, onDelete = ReferenceOption.CASCADE)
val role = enumerationByName("role", 16, FactionRole::class)
val joinedAt = datetime("joined_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(factionId, playerUuid)
}

View File

@ -0,0 +1,24 @@
package net.hareworks.hcu.faction.database.schema
import net.hareworks.hcu.core.database.schema.PlayersTable
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.ReferenceOption
import org.jetbrains.exposed.v1.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.datetime.datetime
enum class FactionRequestType {
INVITE,
REQUEST
}
object FactionRequestsTable : Table("faction_requests") {
val id = long("id").autoIncrement()
val factionId = integer("faction_id")
.references(FactionsTable.id, onDelete = ReferenceOption.CASCADE)
val playerUuid = uuid("player_uuid")
.references(PlayersTable.uuid, onDelete = ReferenceOption.CASCADE)
val type = enumerationByName("type", 16, FactionRequestType::class)
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
}

View File

@ -0,0 +1,17 @@
package net.hareworks.hcu.faction.database.schema
import net.hareworks.hcu.core.database.schema.ActorsTable
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.ReferenceOption
object FactionsTable : Table("factions") {
val id = integer("id")
.references(ActorsTable.actorId, onDelete = ReferenceOption.CASCADE)
val name = varchar("name", 64).uniqueIndex()
val tag = varchar("tag", 16).nullable()
val color = varchar("color", 16).nullable()
val description = text("description").nullable()
val open = bool("open").default(false)
override val primaryKey = PrimaryKey(id)
}

View File

@ -0,0 +1,25 @@
package net.hareworks.hcu.faction.listener
import io.papermc.paper.event.player.AsyncChatEvent
import net.hareworks.hcu.faction.service.FactionService
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
class ChatListener(private val service: FactionService) : Listener {
@EventHandler
fun onChat(event: AsyncChatEvent) {
val tag = service.getFactionTag(event.player.uniqueId) ?: return
val originalRenderer = event.renderer()
event.renderer { source, sourceDisplayName, message, viewer ->
val rendered = originalRenderer.render(source, sourceDisplayName, message, viewer)
Component.text()
.append(Component.text("[$tag] ", NamedTextColor.GOLD))
.append(rendered)
.build()
}
}
}

View File

@ -0,0 +1,41 @@
package net.hareworks.hcu.faction.listener
import net.hareworks.hcu.core.database.DatabaseSessionManager
import net.hareworks.hcu.faction.database.schema.FactionMembersTable
import net.hareworks.hcu.faction.database.schema.FactionRequestsTable
import net.hareworks.hcu.faction.database.schema.FactionsTable
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.server.ServiceRegisterEvent
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import java.util.logging.Level
import java.util.logging.Logger
class DatabaseListener(private val logger: Logger) : Listener {
@EventHandler
fun onServiceRegister(event: ServiceRegisterEvent) {
if (event.provider.service == Database::class.java) {
logger.info("Database service registered. Ensuring Faction schema...")
ensureSchema()
}
}
private fun ensureSchema() {
if (!DatabaseSessionManager.isConnected()) return
try {
DatabaseSessionManager.transaction {
SchemaUtils.createMissingTablesAndColumns(
FactionsTable,
FactionMembersTable,
FactionRequestsTable
)
}
logger.info("Faction schema initialized successfully")
} catch (e: Exception) {
logger.log(Level.SEVERE, "Failed to initialize Faction schema", e)
}
}
}

View File

@ -0,0 +1,412 @@
package net.hareworks.hcu.faction.service
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess
import net.hareworks.hcu.core.database.DatabaseSessionManager
import net.hareworks.hcu.core.database.schema.ActorIdSequence
import net.hareworks.hcu.core.database.schema.ActorsTable
import net.hareworks.hcu.core.database.schema.PlayersTable
import net.hareworks.hcu.faction.database.schema.*
import org.jetbrains.exposed.v1.core.eq
import net.hareworks.hcu.faction.database.schema.*
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.update
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.andWhere
import org.jetbrains.exposed.v1.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.and
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import java.util.UUID
class FactionService {
fun createFaction(name: String, tag: String?, color: String?, ownerUuid: UUID): Result<Int, String> {
return DatabaseSessionManager.transaction {
// Check uniqueness
if (FactionsTable.selectAll().andWhere { FactionsTable.name eq name }.count() > 0L) {
return@transaction Err("Faction name already taken")
}
// Create Actor
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val actorIdVal = ActorsTable.insert {
it[type] = "faction"
it[registeredAt] = today
it[removed] = false
} get ActorsTable.actorId
// Create Faction
FactionsTable.insert {
it[id] = actorIdVal
it[this.name] = name
it[this.tag] = tag
it[this.color] = color
it[open] = false
}
// Add Owner
FactionMembersTable.insert {
it[factionId] = actorIdVal
it[playerUuid] = ownerUuid
it[role] = FactionRole.OWNER
}
Ok(actorIdVal)
}
}
fun getFactionOfPlayer(playerUuid: UUID): Int? {
return DatabaseSessionManager.transaction {
FactionMembersTable
.selectAll()
.andWhere { FactionMembersTable.playerUuid eq playerUuid }
.singleOrNull()
?.get(FactionMembersTable.factionId)
}
}
fun getRole(factionId: Int, playerUuid: UUID): FactionRole? {
return DatabaseSessionManager.transaction {
FactionMembersTable
.selectAll()
.andWhere { FactionMembersTable.factionId eq factionId }
.andWhere { FactionMembersTable.playerUuid eq playerUuid }
.singleOrNull()
?.get(FactionMembersTable.role)
}
}
fun getFactionTag(playerUuid: UUID): String? {
return DatabaseSessionManager.transaction {
(FactionMembersTable innerJoin FactionsTable)
.selectAll()
.andWhere { FactionMembersTable.playerUuid eq playerUuid }
.singleOrNull()
?.get(FactionsTable.tag)
}
}
fun getFactionIdByName(name: String): Int? {
return DatabaseSessionManager.transaction {
FactionsTable
.selectAll()
.andWhere { FactionsTable.name eq name }
.singleOrNull()
?.get(FactionsTable.id)
}
}
fun getFactionName(factionId: Int): String? {
return DatabaseSessionManager.transaction {
FactionsTable
.selectAll()
.andWhere { FactionsTable.id eq factionId }
.singleOrNull()
?.get(FactionsTable.name)
}
}
fun invitePlayer(factionId: Int, targetUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
if (isMember(factionId, targetUuid)) {
return@transaction Err("Player is already a member")
}
if (hasPendingRequest(factionId, targetUuid, FactionRequestType.INVITE)) {
return@transaction Err("Player already invited")
}
FactionRequestsTable.insert {
it[this.factionId] = factionId
it[this.playerUuid] = targetUuid
it[type] = FactionRequestType.INVITE
}
Ok(Unit)
}
}
fun acceptInvite(factionId: Int, playerUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
val request = FactionRequestsTable.selectAll()
.andWhere { FactionRequestsTable.factionId eq factionId }
.andWhere { FactionRequestsTable.playerUuid eq playerUuid }
.andWhere { FactionRequestsTable.type eq FactionRequestType.INVITE }
.singleOrNull() ?: return@transaction Err("No invite found")
addMember(factionId, playerUuid, FactionRole.MEMBER)
val reqId = request[FactionRequestsTable.id]
FactionRequestsTable.deleteWhere { FactionRequestsTable.id eq reqId }
Ok(Unit)
}
}
fun rejectInvite(factionId: Int, playerUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
val request = FactionRequestsTable.selectAll()
.andWhere { FactionRequestsTable.factionId eq factionId }
.andWhere { FactionRequestsTable.playerUuid eq playerUuid }
.andWhere { FactionRequestsTable.type eq FactionRequestType.INVITE }
.singleOrNull() ?: return@transaction Err("No invite found")
val reqId = request[FactionRequestsTable.id]
FactionRequestsTable.deleteWhere { FactionRequestsTable.id eq reqId }
Ok(Unit)
}
}
fun getLatestInvite(playerUuid: UUID): Int? {
return DatabaseSessionManager.transaction {
FactionRequestsTable.selectAll()
.andWhere { FactionRequestsTable.playerUuid eq playerUuid }
.andWhere { FactionRequestsTable.type eq FactionRequestType.INVITE }
.sortedByDescending { it[FactionRequestsTable.createdAt] }
.firstOrNull()
?.get(FactionRequestsTable.factionId)
}
}
private fun addMember(factionId: Int, playerUuid: UUID, role: FactionRole) {
FactionMembersTable.insert {
it[this.factionId] = factionId
it[this.playerUuid] = playerUuid
it[this.role] = role
}
}
private fun isMember(factionId: Int, playerUuid: UUID): Boolean {
return FactionMembersTable.selectAll()
.andWhere { FactionMembersTable.factionId eq factionId }
.andWhere { FactionMembersTable.playerUuid eq playerUuid }
.count() > 0L
}
fun requestJoin(factionId: Int, playerUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
if (isMember(factionId, playerUuid)) return@transaction Err("Already a member")
// If invited, join immediately
if (hasPendingRequest(factionId, playerUuid, FactionRequestType.INVITE)) {
addMember(factionId, playerUuid, FactionRole.MEMBER)
// Delete invite. Find it first to delete by ID to avoid 'and'.
val inviteId = FactionRequestsTable.selectAll()
.andWhere { FactionRequestsTable.factionId eq factionId }
.andWhere { FactionRequestsTable.playerUuid eq playerUuid }
.andWhere { FactionRequestsTable.type eq FactionRequestType.INVITE }
.singleOrNull()?.get(FactionRequestsTable.id)
if (inviteId != null) {
FactionRequestsTable.deleteWhere { FactionRequestsTable.id eq inviteId }
}
return@transaction Ok(Unit)
}
if (hasPendingRequest(factionId, playerUuid, FactionRequestType.REQUEST)) return@transaction Err("Request already pending")
val isOpen = FactionsTable
.selectAll()
.andWhere { FactionsTable.id eq factionId }
.singleOrNull()?.get(FactionsTable.open) ?: return@transaction Err("Faction not found")
if (isOpen) {
addMember(factionId, playerUuid, FactionRole.MEMBER)
Ok(Unit)
} else {
FactionRequestsTable.insert {
it[this.factionId] = factionId
it[this.playerUuid] = playerUuid
it[type] = FactionRequestType.REQUEST
}
Ok(Unit) // Request sent
}
}
}
fun acceptRequest(factionId: Int, targetUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
if (!hasPendingRequest(factionId, targetUuid, FactionRequestType.REQUEST)) {
return@transaction Err("No pending request from this player")
}
addMember(factionId, targetUuid, FactionRole.MEMBER)
val reqId = FactionRequestsTable.selectAll()
.andWhere { FactionRequestsTable.factionId eq factionId }
.andWhere { FactionRequestsTable.playerUuid eq targetUuid }
.andWhere { FactionRequestsTable.type eq FactionRequestType.REQUEST }
.singleOrNull()?.get(FactionRequestsTable.id)
if (reqId != null) {
FactionRequestsTable.deleteWhere { FactionRequestsTable.id eq reqId }
}
Ok(Unit)
}
}
fun setTag(factionId: Int, tag: String): Result<Unit, String> {
return DatabaseSessionManager.transaction {
FactionsTable.update({ FactionsTable.id eq factionId }) {
it[this.tag] = tag
}
Ok(Unit)
}
}
fun setColor(factionId: Int, color: String): Result<Unit, String> {
return DatabaseSessionManager.transaction {
FactionsTable.update({ FactionsTable.id eq factionId }) {
it[this.color] = color
}
Ok(Unit)
}
}
fun setOpen(factionId: Int, open: Boolean): Result<Unit, String> {
return DatabaseSessionManager.transaction {
FactionsTable.update({ FactionsTable.id eq factionId }) {
it[this.open] = open
}
Ok(Unit)
}
}
fun promote(factionId: Int, targetUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
val currentRole = getRole(factionId, targetUuid) ?: return@transaction Err("Player not in faction")
val newRole = when (currentRole) {
FactionRole.MEMBER -> FactionRole.EXEC
FactionRole.EXEC -> return@transaction Err("Cannot promote EXEC, transfer ownership instead")
FactionRole.OWNER -> return@transaction Err("Already OWNER")
}
FactionMembersTable.update({ (FactionMembersTable.factionId eq factionId) and (FactionMembersTable.playerUuid eq targetUuid) }) {
it[role] = newRole
}
Ok(Unit)
}
}
fun demote(factionId: Int, targetUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
val currentRole = getRole(factionId, targetUuid) ?: return@transaction Err("Player not in faction")
val newRole = when (currentRole) {
FactionRole.EXEC -> FactionRole.MEMBER
FactionRole.MEMBER -> return@transaction Err("Cannot demote MEMBER")
FactionRole.OWNER -> return@transaction Err("Cannot demote OWNER")
}
FactionMembersTable.update({ (FactionMembersTable.factionId eq factionId) and (FactionMembersTable.playerUuid eq targetUuid) }) {
it[role] = newRole
}
Ok(Unit)
}
}
private fun hasPendingRequest(factionId: Int, playerUuid: UUID, type: FactionRequestType): Boolean {
return FactionRequestsTable.selectAll()
.andWhere { FactionRequestsTable.factionId eq factionId }
.andWhere { FactionRequestsTable.playerUuid eq playerUuid }
.andWhere { FactionRequestsTable.type eq type }
.count() > 0L
}
fun leaveFaction(playerUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
val factionId = getFactionOfPlayer(playerUuid) ?: return@transaction Err("Not in a faction")
val role = getRole(factionId, playerUuid)
if (role == FactionRole.OWNER) return@transaction Err("Owner cannot leave. Disband or transfer ownership first.")
FactionMembersTable.deleteWhere {
(FactionMembersTable.factionId eq factionId) and (FactionMembersTable.playerUuid eq playerUuid)
}
Ok(Unit)
}
}
fun kickPlayer(actorUuid: UUID, targetUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
val factionId = getFactionOfPlayer(actorUuid) ?: return@transaction Err("You are not in a faction")
val targetFactionId = getFactionOfPlayer(targetUuid)
if (factionId != targetFactionId) return@transaction Err("Player is not in your faction")
val actorRole = getRole(factionId, actorUuid) ?: return@transaction Err("Error fetching role")
val targetRole = getRole(factionId, targetUuid) ?: return@transaction Err("Error fetching target role")
if (actorRole == FactionRole.MEMBER) return@transaction Err("Members cannot kick")
if (actorRole == FactionRole.EXEC && targetRole != FactionRole.MEMBER) return@transaction Err("Execs can only kick members")
if (targetRole == FactionRole.OWNER) return@transaction Err("Cannot kick owner")
FactionMembersTable.deleteWhere {
(FactionMembersTable.factionId eq factionId) and (FactionMembersTable.playerUuid eq targetUuid)
}
Ok(Unit)
}
}
fun disbandFaction(actorUuid: UUID): Result<Unit, String> {
return DatabaseSessionManager.transaction {
val factionId = getFactionOfPlayer(actorUuid) ?: return@transaction Err("Not in a faction")
val role = getRole(factionId, actorUuid)
if (role != FactionRole.OWNER) return@transaction Err("Only owner can disband")
FactionMembersTable.deleteWhere { FactionMembersTable.factionId eq factionId }
FactionRequestsTable.deleteWhere { FactionRequestsTable.factionId eq factionId }
FactionsTable.deleteWhere { FactionsTable.id eq factionId }
ActorsTable.update({ ActorsTable.actorId eq factionId }) {
it[removed] = true
}
Ok(Unit)
}
}
fun getAllFactions(): List<Pair<String, String?>> {
return DatabaseSessionManager.transaction {
FactionsTable.selectAll().map {
it[FactionsTable.name] to it[FactionsTable.tag]
}
}
}
fun getFactionInfo(name: String): FactionInfo? {
return DatabaseSessionManager.transaction {
val row = FactionsTable.selectAll().andWhere { FactionsTable.name eq name }.singleOrNull() ?: return@transaction null
val id = row[FactionsTable.id]
val count = FactionMembersTable.selectAll().andWhere { FactionMembersTable.factionId eq id }.count()
val ownerUuid = FactionMembersTable.selectAll()
.andWhere { FactionMembersTable.factionId eq id }
.andWhere { FactionMembersTable.role eq FactionRole.OWNER }
.singleOrNull()?.get(FactionMembersTable.playerUuid)
FactionInfo(
row[FactionsTable.name],
row[FactionsTable.tag],
row[FactionsTable.color],
row[FactionsTable.open],
count,
ownerUuid
)
}
}
}
data class FactionInfo(
val name: String,
val tag: String?,
val color: String?,
val open: Boolean,
val memberCount: Long,
val owner: UUID?
)