@@ -4,108 +4,248 @@ import dev.slne.surf.cloud.api.common.netty.network.codec.streamCodec
44import dev.slne.surf.cloud.api.common.netty.protocol.buffer.SurfByteBuf
55import dev.slne.surf.cloud.api.common.player.CloudPlayer
66import dev.slne.surf.cloud.api.common.player.CloudPlayerManager
7+ import dev.slne.surf.cloud.api.common.server.UserListImpl.Companion.STREAM_CODEC
78import dev.slne.surf.cloud.api.common.util.annotation.InternalApi
8- import dev.slne.surf.cloud.api.common.util.mutableObjectSetOf
9- import dev.slne.surf.cloud.api.common.util.synchronize
10- import dev.slne.surf.cloud.api.common.util.toObjectSet
11- import it.unimi.dsi.fastutil.objects.ObjectSet
9+ import it.unimi.dsi.fastutil.objects.ObjectArrayList
10+ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
1211import java.util.*
13-
12+ import java.util.concurrent.ConcurrentHashMap
13+ import java.util.concurrent.atomic.AtomicLong
14+
15+ /* *
16+ * Default, high-throughput implementation of [UserList] optimized for
17+ * concurrent reads and frequent iteration in large Minecraft networks.
18+ *
19+ * ### Design rationale
20+ *
21+ * - **Concurrency**: Backed by [ConcurrentHashMap.newKeySet], providing
22+ * fine-grained striping/CAS and weakly-consistent iterators. Readers never block writers, which
23+ * is crucial when plugins may traverse thousands of players while joins/leaves occur.
24+ *
25+ * - **Stable wire format**: Serialization via [STREAM_CODEC] uses a
26+ * snapshot array to ensure a consistent view on the wire even during concurrent updates.
27+ *
28+ * - **Snapshot cache**: A lightweight modification counter ([modSeq]) invalidates a
29+ * cached array snapshot to avoid repeated allocations when the set is unchanged (e.g., multiple
30+ * serializations per tick).
31+ *
32+ * ### Iteration semantics
33+ * Iteration is weakly consistent: it never throws [java.util.ConcurrentModificationException],
34+ * but may omit or not yet include elements added/removed concurrently. Use [uuidSnapshot] for
35+ * a stable, point-in-time collection.
36+ */
1437@InternalApi
1538open class UserListImpl : UserList {
1639 companion object {
40+
41+ /* *
42+ * Binary codec for streaming a [UserListImpl] over [SurfByteBuf].
43+ *
44+ * **Consistency:** The codec writes a snapshot array captured at the start of encoding,
45+ * ensuring readers see a coherent list of UUIDs regardless of concurrent modifications.
46+ */
1747 val STREAM_CODEC = streamCodec<SurfByteBuf , UserListImpl >({ buf, list ->
18- buf.writeCollection(list.playerReferences) { buffer, uuid -> buffer.writeUuid(uuid) }
48+ val snapshot = list.snapshotArray()
49+ buf.writeArray(snapshot) { buffer, uuid -> buffer.writeUuid(uuid) }
1950 }, { buf ->
20- UserListImpl (buf.readCollection({ mutableObjectSetOf(it) }, { it.readUuid() }))
51+ val uuids = buf.readArray { buf.readUuid() }
52+ UserListImpl (ObjectArrayList (uuids))
2153 })
2254
23- fun of (players : Iterable <CloudPlayer >): UserListImpl {
24- return UserListImpl (players.mapTo(mutableObjectSetOf()) { it.uuid })
55+
56+ /* *
57+ * Creates a [UserListImpl] from a preexisting set of UUIDs.
58+ *
59+ * @param uuids UUIDs to initialize the list with.
60+ * @return a new [UserListImpl] containing the given UUIDs.
61+ */
62+ fun of (uuids : Set <UUID >): UserListImpl {
63+ return UserListImpl (ObjectArrayList (uuids))
2564 }
2665 }
2766
28- internal val playerReferences: ObjectSet <UUID >
67+ /* * Backing set: scalable, lock-free key set. */
68+ @PublishedApi
69+ internal val uuids: ConcurrentHashMap .KeySetView <UUID , Boolean >
2970
30- override val size get() = playerReferences.size
31- override val references: ObjectSet <UUID >
32- get() = playerReferences.toObjectSet()
71+ /* * Modification counter used to invalidate the cached snapshot array. */
72+ protected val modSeq = AtomicLong (0 )
3373
74+ @Volatile
75+ private var cachedSnapshot: Array <UUID > = emptyArray()
76+
77+ @Volatile
78+ private var cachedModSeq: Long = - 1
79+
80+ /* * Creates an empty user list. */
3481 constructor () {
35- playerReferences = mutableObjectSetOf <UUID >().synchronize ()
82+ uuids = ConcurrentHashMap .newKeySet <UUID >()
3683 }
3784
38- internal constructor (playerReferences: ObjectSet <UUID >) {
39- this .playerReferences = playerReferences.synchronize()
85+ /* *
86+ * Creates a user list preloaded with UUIDs.
87+ *
88+ * @param initial UUIDs to preload into the set.
89+ */
90+ internal constructor (initial: ObjectArrayList <UUID >) {
91+ uuids = ConcurrentHashMap .newKeySet<UUID >(initial.size)
92+ uuids.addAll(initial)
4093 }
4194
42- override fun contains (element : CloudPlayer ) = playerReferences.contains(element.uuid)
43- override fun containsAll (elements : Collection <CloudPlayer >) =
44- elements.all { playerReferences.contains(it.uuid) }
95+ override val size
96+ get() = uuids.size
4597
46- override fun isEmpty () = playerReferences.isEmpty()
47- override fun iterator (): Iterator <CloudPlayer > {
48- return object : Iterator <CloudPlayer > {
49- private val iterator = playerReferences.iterator()
50- override fun hasNext () = iterator.hasNext()
51- override fun next () = CloudPlayerManager .getPlayer(iterator.next())
52- ? : throw NoSuchElementException (" Player not found" )
53- }
98+ override fun isEmpty () = uuids.isEmpty()
99+ override fun contains (element : CloudPlayer ) = uuids.contains(element.uuid)
100+ override fun containsAll (elements : Collection <CloudPlayer >): Boolean {
101+ for (e in elements) if (! uuids.contains(e.uuid)) return false
102+ return true
54103 }
55104
56- fun add (playerUuid : UUID ): Boolean {
57- return playerReferences.add(playerUuid)
105+ /* *
106+ * Returns an iterator over players resolved on demand from [CloudPlayerManager].
107+ *
108+ * **Note:** If a UUID in the set cannot be resolved to a [CloudPlayer]
109+ * (e.g., the player disconnected between reading the UUID and resolution),
110+ * a [NoSuchElementException] is thrown from [next] to signal the race.
111+ *
112+ * @throws NoSuchElementException if a UUID cannot be resolved to a player at iteration time.
113+ */
114+ override fun iterator () = object : Iterator <CloudPlayer > {
115+ private val iterator = uuids.iterator()
116+ override fun hasNext () = iterator.hasNext()
117+ override fun next () = CloudPlayerManager .getPlayer(iterator.next())
118+ ? : throw NoSuchElementException (" Player not found" )
58119 }
59120
60- fun remove (playerUuid : UUID ): Boolean {
61- return playerReferences.remove(playerUuid)
121+ /* *
122+ * Implementation-specific fast traversal over UUIDs using the weakly-consistent iterator.
123+ *
124+ * @param action callback invoked for each UUID visible to the iterator.
125+ */
126+ inline fun forEachWeakUuid (action : (UUID ) -> Unit ) {
127+ for (u in uuids) action(u)
62128 }
63129
64130 override fun snapshot (): MutableUserList {
65- return MutableUserListImpl ().also { it.addAll(this ) }
131+ return MutableUserListImpl (uuidSnapshot())
132+ }
133+
134+ override fun uuidSnapshot () = ObjectArrayList (snapshotArray())
135+
136+ /* *
137+ * Returns a cached snapshot array of UUIDs, invalidated whenever the set changes.
138+ *
139+ * **Performance:** Avoids repeated allocations between unchanged serializations.
140+ * The snapshot is coherent at the time of creation and unaffected by later mutations.
141+ *
142+ * @return array of UUIDs representing a point-in-time view.
143+ */
144+ fun snapshotArray (): Array <UUID > {
145+ val m = modSeq.get()
146+ if (m == cachedModSeq) return cachedSnapshot
147+ val snap = uuids.toTypedArray()
148+ cachedSnapshot = snap
149+ cachedModSeq = m
150+ return snap
151+ }
152+
153+
154+ /* *
155+ * Adds a player UUID to the live set.
156+ *
157+ * @param playerUuid the UUID to add.
158+ * @return `true` if the UUID was not present and has been added; `false` otherwise.
159+ */
160+ fun add (playerUuid : UUID ): Boolean {
161+ val added = uuids.add(playerUuid)
162+ if (added) {
163+ modSeq.incrementAndGet()
164+ }
165+ return added
166+ }
167+
168+ /* *
169+ * Removes a player UUID from the live set.
170+ *
171+ * @param playerUuid the UUID to remove.
172+ * @return `true` if the UUID was present and has been removed; `false` otherwise.
173+ */
174+ fun remove (playerUuid : UUID ): Boolean {
175+ val removed = uuids.remove(playerUuid)
176+ if (removed) {
177+ modSeq.incrementAndGet()
178+ }
179+ return removed
66180 }
67181
68182 override fun toString (): String {
69- return " UserListImpl(playerReferences= $playerReferences , size=$size )"
183+ return " UserListImpl(size=$size )"
70184 }
71185}
72186
187+
188+ /* *
189+ * Mutable snapshot implementation that can be freely modified without impacting the live set.
190+ *
191+ * ### Notes
192+ *
193+ * - [clear] increments [modSeq] to invalidate the internal snapshot cache.
194+ * - Mutating via the iterator's [MutableIterator.remove] also bumps [modSeq]
195+ * to ensure consistent snapshot invalidation.
196+ * - [retainAll] builds a temporary [ObjectOpenHashSet] of UUIDs to perform the set
197+ * operation efficiently.
198+ */
73199@InternalApi
74- class MutableUserListImpl : UserListImpl (), MutableUserList {
200+ class MutableUserListImpl ( initial : ObjectArrayList < UUID >) : UserListImpl(initial ), MutableUserList {
75201 override fun add (element : CloudPlayer ): Boolean {
76202 return add(element.uuid)
77203 }
78204
79205 override fun addAll (elements : Collection <CloudPlayer >): Boolean {
80- return elements.all { playerReferences.add(it.uuid) }
206+ var changed = false
207+ for (e in elements) if (add(e.uuid)) changed = true
208+ return changed
81209 }
82210
83211 override fun clear () {
84- playerReferences.clear()
212+ if (uuids.isNotEmpty()) {
213+ uuids.clear()
214+ modSeq.incrementAndGet()
215+ }
85216 }
86217
87- override fun iterator (): MutableIterator <CloudPlayer > {
88- return object : MutableIterator <CloudPlayer > {
89- private val iterator = playerReferences.iterator()
90- override fun hasNext () = iterator.hasNext()
91- override fun next () = CloudPlayerManager .getPlayer(iterator.next())
92- ? : throw NoSuchElementException (" Player not found" )
93-
94- override fun remove () {
95- iterator.remove()
96- }
218+ /* *
219+ * Iterator over players resolved on demand. Removal through the iterator
220+ * invalidates the snapshot cache to keep [snapshotArray] fresh.
221+ */
222+ override fun iterator () = object : MutableIterator <CloudPlayer > {
223+ private val iterator = uuids.iterator()
224+ override fun hasNext () = iterator.hasNext()
225+ override fun next () = CloudPlayerManager .getPlayer(iterator.next())
226+ ? : throw NoSuchElementException (" Player not found" )
227+
228+ override fun remove () {
229+ iterator.remove()
230+ modSeq.incrementAndGet()
97231 }
98232 }
99233
234+
100235 override fun remove (element : CloudPlayer ): Boolean {
101236 return remove(element.uuid)
102237 }
103238
104239 override fun removeAll (elements : Collection <CloudPlayer >): Boolean {
105- return elements.all { playerReferences.remove(it.uuid) }
240+ var changed = false
241+ for (e in elements) if (remove(e.uuid)) changed = true
242+ return changed
106243 }
107244
108245 override fun retainAll (elements : Collection <CloudPlayer >): Boolean {
109- return playerReferences.retainAll(elements.map { it.uuid })
246+ val keep = ObjectOpenHashSet <UUID >(elements.size)
247+ for (e in elements) keep.add(e.uuid)
248+ val changed = uuids.retainAll(keep)
249+ return changed
110250 }
111251}
0 commit comments