Skip to content

Commit 8a431b9

Browse files
Add party system with in-memory and Redis APIs
Introduces a modular party system for KPaper, including a read-only PartyAPI interface, a default in-memory implementation, and a Redis-backed implementation. Adds customizable Party GUI support and updates dependencies to include Jedis and Gson for Redis integration.
1 parent 8964501 commit 8a431b9

File tree

6 files changed

+643
-0
lines changed

6 files changed

+643
-0
lines changed

build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ dependencies {
3434

3535
api("com.squareup.okhttp3:okhttp:4.12.0")
3636

37+
// Redis client for Redis-backed PartyAPI implementation (Jedis)
38+
implementation("redis.clients:jedis:7.1.0")
39+
3740
testImplementation("io.kotest:kotest-runner-junit5:$koTestVersion")
3841
testImplementation("io.mockk:mockk:${mockkVersion}")
3942
testImplementation("com.google.code.gson:gson:2.11.0")
43+
implementation("com.google.code.gson:gson:2.11.0")
4044
}
4145

4246
paperweight {
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package cc.modlabs.kpaper.party
2+
3+
import cc.modlabs.kpaper.inventory.GUI
4+
import cc.modlabs.kpaper.inventory.ItemBuilder
5+
import dev.fruxz.stacked.text
6+
import org.bukkit.Bukkit
7+
import org.bukkit.Material
8+
import org.bukkit.OfflinePlayer
9+
import org.bukkit.entity.Player
10+
import java.time.Duration
11+
import java.util.Optional
12+
import java.util.UUID
13+
import java.util.concurrent.CompletableFuture
14+
import java.util.concurrent.ConcurrentHashMap
15+
import java.util.concurrent.CopyOnWriteArraySet
16+
17+
/**
18+
* A simple in-memory implementation of [PartyAPI].
19+
*
20+
* Note: This implementation is NOT persistent and serves as a default/fallback.
21+
* Projects can provide their own implementation (e.g. Redis) via [Party.api].
22+
*/
23+
class DefaultPartyAPI : PartyAPI {
24+
25+
private data class InternalParty(
26+
val id: String,
27+
@Volatile var leader: UUID,
28+
val members: MutableSet<UUID> = CopyOnWriteArraySet(),
29+
val createdAt: Long = System.currentTimeMillis(),
30+
@Volatile var maxSize: Int = 8
31+
)
32+
33+
private data class InviteKey(val partyId: String, val playerId: UUID)
34+
35+
private val parties = ConcurrentHashMap<String, InternalParty>()
36+
private val playerToParty = ConcurrentHashMap<UUID, String>()
37+
private val invites = ConcurrentHashMap<InviteKey, PartyAPI.PartyInvite>()
38+
39+
// --------------- Party checks -----------------
40+
41+
override fun isInParty(playerId: UUID): CompletableFuture<Boolean> =
42+
CompletableFuture.completedFuture(playerToParty.containsKey(playerId))
43+
44+
override fun isInParty(player: OfflinePlayer): CompletableFuture<Boolean> =
45+
isInParty(player.uniqueId)
46+
47+
override fun isPartyLeader(playerId: UUID): CompletableFuture<Boolean> =
48+
CompletableFuture.completedFuture(
49+
playerToParty[playerId]?.let { parties[it]?.leader == playerId } ?: false
50+
)
51+
52+
override fun areInSameParty(player1: UUID, player2: UUID): CompletableFuture<Boolean> =
53+
CompletableFuture.completedFuture(playerToParty[player1] != null && playerToParty[player1] == playerToParty[player2])
54+
55+
override fun getPartyId(playerId: UUID): CompletableFuture<Optional<String>> =
56+
CompletableFuture.completedFuture(Optional.ofNullable(playerToParty[playerId]))
57+
58+
// --------------- Party data -----------------
59+
60+
override fun getPartyData(partyId: String): CompletableFuture<Optional<PartyAPI.PartyData>> =
61+
CompletableFuture.completedFuture(Optional.ofNullable(parties[partyId]?.toApi()))
62+
63+
override fun getPlayerParty(playerId: UUID): CompletableFuture<Optional<PartyAPI.PartyData>> =
64+
CompletableFuture.completedFuture(
65+
Optional.ofNullable(playerToParty[playerId]?.let { parties[it]?.toApi() })
66+
)
67+
68+
override fun getPartyLeader(playerId: UUID): CompletableFuture<Optional<UUID>> =
69+
CompletableFuture.completedFuture(
70+
Optional.ofNullable(playerToParty[playerId]?.let { parties[it]?.leader })
71+
)
72+
73+
override fun getPartyMembers(partyId: String): CompletableFuture<Set<UUID>> =
74+
CompletableFuture.completedFuture(parties[partyId]?.members?.toSet() ?: emptySet())
75+
76+
override fun getPartyMembersOfPlayer(playerId: UUID): CompletableFuture<Set<UUID>> =
77+
CompletableFuture.completedFuture(
78+
playerToParty[playerId]?.let { parties[it]?.members?.toSet() } ?: emptySet()
79+
)
80+
81+
override fun getPartyMemberCount(partyId: String): CompletableFuture<Int> =
82+
CompletableFuture.completedFuture(parties[partyId]?.members?.size ?: 0)
83+
84+
override fun isPartyFull(partyId: String): CompletableFuture<Boolean> =
85+
CompletableFuture.completedFuture(parties[partyId]?.let { it.members.size >= it.maxSize } ?: false)
86+
87+
override fun hasInvite(playerId: UUID, partyId: String): CompletableFuture<Boolean> =
88+
CompletableFuture.completedFuture(invites[InviteKey(partyId, playerId)]?.let { !it.isExpired() } ?: false)
89+
90+
override fun getInvite(playerId: UUID, partyId: String): CompletableFuture<Optional<PartyAPI.PartyInvite>> =
91+
CompletableFuture.completedFuture(
92+
Optional.ofNullable(invites[InviteKey(partyId, playerId)]?.takeUnless { it.isExpired() })
93+
)
94+
95+
// --------------- Invites -----------------
96+
97+
override fun getAllInvites(playerId: UUID): CompletableFuture<Set<String>> =
98+
CompletableFuture.completedFuture(
99+
invites.filter { it.key.playerId == playerId && !it.value.isExpired() }.keys.map { it.partyId }.toSet()
100+
)
101+
102+
override fun partyExists(partyId: String): CompletableFuture<Boolean> =
103+
CompletableFuture.completedFuture(parties.containsKey(partyId))
104+
105+
override fun getOnlinePartyMembers(partyId: String): CompletableFuture<Set<UUID>> =
106+
CompletableFuture.completedFuture(
107+
parties[partyId]?.members?.filter { Bukkit.getPlayer(it) != null }?.toSet() ?: emptySet()
108+
)
109+
110+
// --------------- Utilities -----------------
111+
112+
override fun getOnlinePartyMemberCount(partyId: String): CompletableFuture<Int> =
113+
CompletableFuture.completedFuture(getOnlineCount(partyId))
114+
115+
override fun openPartyGUI(player: Player) {
116+
val partyId = playerToParty[player.uniqueId]
117+
val party = partyId?.let { parties[it]?.toApi() }
118+
val inv = PartyGUI.factory.build(player, party)
119+
player.openInventory(inv)
120+
}
121+
122+
// --------------- Internal helpers -----------------
123+
124+
private fun InternalParty.toApi(): PartyAPI.PartyData = PartyAPI.PartyData(
125+
partyId = id,
126+
leader = leader,
127+
members = members.toSet(),
128+
createdAt = createdAt,
129+
maxSize = maxSize
130+
)
131+
132+
private fun getOnlineCount(partyId: String): Int =
133+
parties[partyId]?.members?.count { Bukkit.getPlayer(it) != null } ?: 0
134+
135+
// --------------- Minimal management API (internal) -----------------
136+
// Not part of PartyAPI (which is read-only), but useful for tests/examples
137+
138+
fun createParty(id: String, leader: UUID, maxSize: Int = 8): PartyAPI.PartyData {
139+
val p = InternalParty(id, leader, mutableSetOf<UUID>().apply { add(leader) }, System.currentTimeMillis(), maxSize)
140+
parties[id] = p
141+
playerToParty[leader] = id
142+
return p.toApi()
143+
}
144+
145+
fun addMember(partyId: String, playerId: UUID): Boolean {
146+
val p = parties[partyId] ?: return false
147+
if (p.members.size >= p.maxSize) return false
148+
p.members.add(playerId)
149+
playerToParty[playerId] = partyId
150+
return true
151+
}
152+
153+
fun removeMember(playerId: UUID) {
154+
val pid = playerToParty.remove(playerId) ?: return
155+
val p = parties[pid] ?: return
156+
p.members.remove(playerId)
157+
if (playerId == p.leader) {
158+
// Promote someone else or disband
159+
val newLeader = p.members.firstOrNull()
160+
if (newLeader == null) {
161+
parties.remove(pid)
162+
} else {
163+
p.leader = newLeader
164+
}
165+
}
166+
}
167+
168+
fun invite(playerId: UUID, partyId: String, inviter: UUID, ttl: Duration = Duration.ofSeconds(60)) {
169+
val invite = PartyAPI.PartyInvite(
170+
partyId = partyId,
171+
invitedPlayer = playerId,
172+
inviter = inviter,
173+
createdAt = System.currentTimeMillis(),
174+
expiresAt = System.currentTimeMillis() + ttl.toMillis()
175+
)
176+
invites[InviteKey(partyId, playerId)] = invite
177+
}
178+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package cc.modlabs.kpaper.party
2+
3+
/**
4+
* Central access point for the party system in KPaper.
5+
*
6+
* Projects can replace [api] at runtime with their own implementation
7+
* (e.g. a Redis-backed remote API). By default [DefaultPartyAPI] is used,
8+
* which is in-memory and non-persistent.
9+
*/
10+
object Party {
11+
@Volatile
12+
var api: PartyAPI = DefaultPartyAPI()
13+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package cc.modlabs.kpaper.party
2+
3+
import org.bukkit.OfflinePlayer
4+
import org.bukkit.entity.Player
5+
import java.util.Optional
6+
import java.util.UUID
7+
import java.util.concurrent.CompletableFuture
8+
9+
/**
10+
* Party API for external plugins.
11+
* Provides read-only access to the party system.
12+
*
13+
* Note: In KPaper this is an in-memory implementation by default.
14+
* Projects can register their own implementation (e.g. Redis) via [Party.api].
15+
*/
16+
interface PartyAPI {
17+
18+
// ==================== Party checks ====================
19+
20+
/**
21+
* Checks whether a player is in a party.
22+
*/
23+
fun isInParty(playerId: UUID): CompletableFuture<Boolean>
24+
25+
/**
26+
* Checks whether a player is in a party.
27+
*/
28+
fun isInParty(player: OfflinePlayer): CompletableFuture<Boolean> = isInParty(player.uniqueId)
29+
30+
/**
31+
* Checks whether a player is the party leader.
32+
*/
33+
fun isPartyLeader(playerId: UUID): CompletableFuture<Boolean>
34+
35+
/**
36+
* Checks whether two players are in the same party.
37+
*/
38+
fun areInSameParty(player1: UUID, player2: UUID): CompletableFuture<Boolean>
39+
40+
/**
41+
* Gets the party ID of a player.
42+
*/
43+
fun getPartyId(playerId: UUID): CompletableFuture<Optional<String>>
44+
45+
/**
46+
* Gets the party ID of a player.
47+
*/
48+
fun getPartyId(player: OfflinePlayer): CompletableFuture<Optional<String>> = getPartyId(player.uniqueId)
49+
50+
// ==================== Party data ====================
51+
52+
/**
53+
* Gets the full party data by party ID.
54+
*/
55+
fun getPartyData(partyId: String): CompletableFuture<Optional<PartyData>>
56+
57+
/**
58+
* Gets the party data of a player.
59+
*/
60+
fun getPlayerParty(playerId: UUID): CompletableFuture<Optional<PartyData>>
61+
62+
/**
63+
* Gets the party data of a player.
64+
*/
65+
fun getPlayerParty(player: OfflinePlayer): CompletableFuture<Optional<PartyData>> = getPlayerParty(player.uniqueId)
66+
67+
/**
68+
* Gets the party leader of a player’s party.
69+
*/
70+
fun getPartyLeader(playerId: UUID): CompletableFuture<Optional<UUID>>
71+
72+
/**
73+
* Gets all members of a party.
74+
*/
75+
fun getPartyMembers(partyId: String): CompletableFuture<Set<UUID>>
76+
77+
/**
78+
* Gets all members of the party a player is in.
79+
*/
80+
fun getPartyMembersOfPlayer(playerId: UUID): CompletableFuture<Set<UUID>>
81+
82+
/**
83+
* Counts the number of members in a party.
84+
*/
85+
fun getPartyMemberCount(partyId: String): CompletableFuture<Int>
86+
87+
/**
88+
* Checks whether a party is full.
89+
*/
90+
fun isPartyFull(partyId: String): CompletableFuture<Boolean>
91+
92+
/**
93+
* Checks whether a player has a party invite.
94+
*/
95+
fun hasInvite(playerId: UUID, partyId: String): CompletableFuture<Boolean>
96+
97+
/**
98+
* Gets a party invite.
99+
*/
100+
fun getInvite(playerId: UUID, partyId: String): CompletableFuture<Optional<PartyInvite>>
101+
102+
// ==================== Party invites ====================
103+
104+
/**
105+
* Gets all open party invites of a player.
106+
*/
107+
fun getAllInvites(playerId: UUID): CompletableFuture<Set<String>>
108+
109+
/**
110+
* Checks whether a party exists.
111+
*/
112+
fun partyExists(partyId: String): CompletableFuture<Boolean>
113+
114+
/**
115+
* Gets all online members of a party.
116+
*/
117+
fun getOnlinePartyMembers(partyId: String): CompletableFuture<Set<UUID>>
118+
119+
// ==================== Party utilities ====================
120+
121+
/**
122+
* Counts the number of online members in a party.
123+
*/
124+
fun getOnlinePartyMemberCount(partyId: String): CompletableFuture<Int>
125+
126+
/**
127+
* Opens the party GUI for a player. Only works if the player is online.
128+
*/
129+
fun openPartyGUI(player: Player)
130+
131+
// ==================== Data containers ====================
132+
133+
data class PartyData(
134+
val partyId: String,
135+
val leader: UUID,
136+
val members: Set<UUID>,
137+
val createdAt: Long,
138+
val maxSize: Int
139+
) {
140+
fun isMember(playerId: UUID): Boolean = members.contains(playerId)
141+
fun isLeader(playerId: UUID): Boolean = leader == playerId
142+
fun getMemberCount(): Int = members.size
143+
fun isFull(): Boolean = members.size >= maxSize
144+
}
145+
146+
data class PartyInvite(
147+
val partyId: String,
148+
val invitedPlayer: UUID,
149+
val inviter: UUID,
150+
val createdAt: Long,
151+
val expiresAt: Long
152+
) {
153+
fun isExpired(): Boolean = System.currentTimeMillis() > expiresAt
154+
}
155+
}

0 commit comments

Comments
 (0)