Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dev.slne.surf.api.gradle.util.slneReleases
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmExtension

buildscript {
Expand Down
40 changes: 39 additions & 1 deletion surf-clan-api/src/main/kotlin/dev/slne/clan/api/clan/Clan.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.slne.clan.api.clan

import dev.slne.clan.api.clan.listener.ClanListener
import dev.slne.clan.api.clan.update.ClanNameAndTag
import dev.slne.clan.api.invite.ClanInvite
import dev.slne.clan.api.invite.ClanInviteResult
import dev.slne.clan.api.member.ClanMember
Expand Down Expand Up @@ -45,6 +46,16 @@ interface Clan : ClanView {

suspend fun changeClanTagColor(update: ClanTagColor.Update)

/**
* Updates the clan's name and tag.
*/
suspend fun updateClanNameAndTag(update: ClanNameAndTag.Update): ClanNameAndTag.UpdateResult

/**
* Tests whether a proposed clan name and tag update would be valid without actually applying the changes.
*/
suspend fun testClanNameAndTagUpdate(update: ClanNameAndTag.Update): ClanNameAndTag.UpdateResult

/**
* Retrieves all pending invitations sent by this clan.
*
Expand Down Expand Up @@ -163,6 +174,11 @@ interface Clan : ClanView {
*/
const val MAX_TAG_LENGTH = 4

/**
* The cost for renaming a clan.
*/
const val CLAN_RENAME_COST = 5_000.0

/**
* Registers a listener to receive clan-related events.
*
Expand Down Expand Up @@ -205,6 +221,14 @@ interface Clan : ClanView {
*/
suspend fun byTag(tag: String): Clan? = ClanService.findClanByTag(tag)

/**
* Retrieves a clan by its name.
*
* @param name the clan name to search for
* @return the [Clan] if found, or `null` otherwise
*/
suspend fun byName(name: String): Clan? = ClanService.findClanByName(name)

/**
* Validates a clan name and tag against system rules.
*
Expand Down Expand Up @@ -265,4 +289,18 @@ suspend inline fun Clan.changeClanTagColor(
update: ClanTagColor.Update.Builder.() -> Unit
) = changeClanTagColor(
ClanTagColor.update(update)
)
)

/**
* Updates the clan's name and tag.
*/
suspend inline fun Clan.updateClanNameAndTag(
update: ClanNameAndTag.Update.Builder.() -> Unit
) = updateClanNameAndTag(ClanNameAndTag.update(update))

/**
* @see Clan.testClanNameAndTagUpdate
*/
suspend inline fun Clan.testClanNameAndTagUpdate(
update: ClanNameAndTag.Update.Builder.() -> Unit
) = testClanNameAndTagUpdate(ClanNameAndTag.update(update))
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ClanService {
suspend fun findClanByPlayer(playerUuid: UUID): Clan?
suspend fun findClanByUuid(clanUuid: UUID): Clan?
suspend fun findClanByTag(tag: String): Clan?
suspend fun findClanByName(name: String): Clan?

fun validateClanNameAndTag(name: String, tag: String): ClanValidationResult
suspend fun createClan(properties: ClanCreateBuilder): ClanCreationResult
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package dev.slne.clan.api.clan.update

import dev.slne.clan.api.clan.ClanValidationResult
import dev.slne.clan.api.util.InternalClanApi
import kotlinx.serialization.Serializable

@Serializable
data class ClanNameAndTag(
val name: String,
val tag: String
) {

@ConsistentCopyVisibility
data class Update private constructor(
internal val name: Field<String>,
internal val tag: Field<String>
) {
class Builder @PublishedApi internal constructor() {
private var name: Field<String> = Field.Unset
private var tag: Field<String> = Field.Unset

fun name(name: String) {
this.name = Field.Set(name)
}

fun tag(tag: String) {
this.tag = Field.Set(tag)
}

@PublishedApi
internal fun build() = Update(name, tag)
}

fun hasUpdates() = name != Field.Unset || tag != Field.Unset

@InternalClanApi
fun changedNameOrNull() = (name as? Field.Set<String>)?.value

@InternalClanApi
fun changedTagOrNull() = (tag as? Field.Set<String>)?.value
}

@Serializable
sealed interface UpdateResult {
val isSuccess: Boolean

@Serializable
data object UpdatedName : UpdateResult {
override val isSuccess: Boolean = true
}

@Serializable
data object UpdatedTag : UpdateResult {
override val isSuccess: Boolean = true
}

@Serializable
data object UpdatedNameAndTag : UpdateResult {
override val isSuccess: Boolean = true
}

@Serializable
data object NothingChanged : UpdateResult {
override val isSuccess: Boolean = false
}

@Serializable
data object NameAlreadyTaken : UpdateResult {
override val isSuccess: Boolean = false
}

@Serializable
data object TagAlreadyTaken : UpdateResult {
override val isSuccess: Boolean = false
}

@Serializable
data object TagOrNameAlreadyTaken : UpdateResult {
override val isSuccess: Boolean = false
}

@Serializable
data class ValidationFailed(val result: ClanValidationResult) : UpdateResult {
override val isSuccess: Boolean = false
}
}

companion object {
inline fun update(block: Update.Builder.() -> Unit): Update {
return Update.Builder().apply(block).build()
}
}

internal sealed interface Field<out T> {
data object Unset : Field<Nothing>
data class Set<T>(val value: T) : Field<T>
}

fun applyUpdate(update: Update): ClanNameAndTag {
fun <T> Field<T>.resolve(current: T): T =
when (this) {
Field.Unset -> current
is Field.Set -> value
}

return copy(
name = update.name.resolve(name),
tag = update.tag.resolve(tag)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ enum class ClanMemberRole(
ClanPermission.DEMOTE,
ClanPermission.PROMOTE,
ClanPermission.DISCORD,
ClanPermission.OPTIONS_TAG_COLOR
ClanPermission.OPTIONS_TAG_COLOR,
ClanPermission.RENAME
);

private val permissions = EnumSet.noneOf(ClanPermission::class.java).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,10 @@ enum class ClanPermission {
/**
* Permission to change the color of the clan tag.
*/
OPTIONS_TAG_COLOR
OPTIONS_TAG_COLOR,

/**
* Permission to change the clan's name and tag.
*/
RENAME
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.slne.clan.api.util

import dev.slne.surf.api.shared.api.annotation.InternalAPIMarker

/**
* Marks declarations that are internal to the Surf Clan API implementation.
*
Expand All @@ -19,4 +21,5 @@ package dev.slne.clan.api.util
message = "This API is internal to the Surf Clan implementation and is not intended for external use. " +
"It may be changed or removed without notice in future versions."
)
@InternalAPIMarker
annotation class InternalClanApi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.slne.surf.clan.core.clan

import dev.slne.clan.api.clan.Clan
import dev.slne.clan.api.clan.ClanTagColor
import dev.slne.clan.api.clan.update.ClanNameAndTag
import dev.slne.clan.api.invite.ClanInvite
import dev.slne.clan.api.invite.ClanInviteResult
import dev.slne.clan.api.member.ClanMember
Expand All @@ -18,8 +19,8 @@ import java.util.*
data class ClanImpl(
override val clanID: ULong,
override val uuid: @Contextual UUID,
override val name: String,
override val tag: String,
override var name: String,
override var tag: String,
override val createdByUuid: @Contextual UUID,
override var clanTagColor: ClanTagColor?,
override var description: String?,
Expand All @@ -40,6 +41,14 @@ data class ClanImpl(
CoreClanService.updateTagColor(this, update)
}

override suspend fun updateClanNameAndTag(update: ClanNameAndTag.Update): ClanNameAndTag.UpdateResult {
return CoreClanService.updateClanNameAndTag(this, update)
}

override suspend fun testClanNameAndTagUpdate(update: ClanNameAndTag.Update): ClanNameAndTag.UpdateResult {
return CoreClanService.testClanNameAndTagUpdate(this, update)
}

override suspend fun getPendingInvites(): Set<ClanInvite> {
return CoreClanService.fetchPendingInvites(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.slne.surf.clan.core.clan

import dev.slne.clan.api.clan.ClanService
import dev.slne.clan.api.clan.ClanTagColor
import dev.slne.clan.api.clan.update.ClanNameAndTag
import dev.slne.clan.api.invite.ClanInviteResult
import dev.slne.clan.api.member.ClanMemberAddResult
import dev.slne.clan.api.member.ClanMemberRole
Expand All @@ -20,6 +21,8 @@ interface CoreClanService : ClanService {
suspend fun updateDescription(clan: ClanImpl, description: String?): Boolean
suspend fun updateDiscordInvite(clan: ClanImpl, discordInvite: String?): Boolean
suspend fun updateTagColor(clan: ClanImpl, update: ClanTagColor.Update): Boolean
suspend fun updateClanNameAndTag(clan: ClanImpl, update: ClanNameAndTag.Update): ClanNameAndTag.UpdateResult
suspend fun testClanNameAndTagUpdate(clan: ClanImpl, update: ClanNameAndTag.Update): ClanNameAndTag.UpdateResult

suspend fun fetchPendingInvites(clan: AbstractClanView): Set<ClanInviteImpl>
suspend fun invitePlayer(clan: ClanImpl, invitee: UUID, invitedBy: UUID): ClanInviteResult
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.slne.surf.clan.core.rpc

import dev.slne.clan.api.clan.ClanCreationResult
import dev.slne.clan.api.clan.update.ClanNameAndTag
import dev.slne.surf.clan.core.clan.ClanImpl
import dev.slne.surf.rabbitmq.api.rpc.RpcService
import net.kyori.adventure.text.format.ShadowColor
Expand All @@ -27,6 +28,7 @@ interface ClanRpcService {
suspend fun findClanById(clanID: ULong): ClanImpl?
suspend fun findClanByMember(memberUuid: UUID): ClanImpl?
suspend fun findClanByTag(tag: String): ClanImpl?
suspend fun findClanByName(name: String): ClanImpl?
suspend fun findClanByClanUuid(clanUuid: UUID): ClanImpl?
suspend fun findClanTagsByPrefix(prefix: String, limit: Int): List<String>

Expand All @@ -38,4 +40,10 @@ interface ClanRpcService {
tagBackgroundColor: TextColor?,
tagShadowColor: ShadowColor?
): Boolean

suspend fun updateClanNameAndTag(
clanID: ULong,
name: String?,
tag: String?
): ClanNameAndTag.UpdateResult
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.slne.surf.clan.core.client.components

import dev.slne.clan.api.clan.Clan.Companion.DISCORD_LINK_REQUIRED_MEMBERS
import dev.slne.clan.api.clan.ClanValidationResult
import dev.slne.clan.api.member.ClanMemberRole
import dev.slne.surf.api.core.font.toSmallCaps
import dev.slne.surf.api.core.messages.Colors
Expand Down Expand Up @@ -85,6 +86,52 @@ object Components {
}
}
}

fun renderClanValidation(result: ClanValidationResult) = buildText {
when (result) {
is ClanValidationResult.NameOutOfRange -> {
error("Der Name muss zwischen ")
variableValue(result.min)
error(" und ")
variableValue(result.max)
error(" Zeichen lang sein.")
}

is ClanValidationResult.TagOutOfRange -> {
error("Der Tag muss zwischen ")
variableValue(result.min)
error(" und ")
variableValue(result.max)
error(" Zeichen lang sein.")
}

is ClanValidationResult.InvalidTagCharacters -> {
error("Der Clan-Tag enthält ungültige Zeichen: ")
for (entry in result.characters.char2BooleanEntrySet()) {
val char = entry.charKey
val valid = entry.booleanValue

if (valid) {
text(char, Colors.GRAY)
} else {
append {
text(char, Colors.ERROR)
decorate(TextDecoration.UNDERLINED)
}
}
}

appendNewErrorPrefixedLine()
error("Er darf nur aus Buchstaben und Zahlen bestehen.")
}

ClanValidationResult.TagViolation -> {
error("Der Clan-Tag ist nicht erlaubt.")
}

else -> Unit
}
}
}

private fun SurfComponentBuilder.appendLine(key: String, value: Any) = append {
Expand Down
Loading