A Minecraft Fabric mod that connects the game with ATProtocol ⛏️
1package com.jollywhoppers.atproto.examples
2
3import com.jollywhoppers.atproto.RecordManager
4import com.jollywhoppers.atproto.AtProtoSessionManager
5import kotlinx.serialization.Serializable
6import kotlinx.serialization.serializer
7import org.slf4j.LoggerFactory
8import java.util.*
9
10/**
11 * Comprehensive examples demonstrating the RecordManager API.
12 * Shows all CRUD operations and best practices.
13 */
14class RecordManagerExamples(
15 private val sessionManager: AtProtoSessionManager
16) {
17 private val logger = LoggerFactory.getLogger("atproto-connect:Examples")
18 private val recordManager = RecordManager(sessionManager)
19
20 // ============================================================================
21 // EXAMPLE 1: Creating Records with Auto-Generated TIDs
22 // ============================================================================
23
24 /**
25 * Example: Create a player stats snapshot
26 * Uses createRecord for automatic TID generation (timestamp-based key)
27 */
28 suspend fun createPlayerStats(playerUuid: UUID): Result<String> = runCatching {
29 val stats = PlayerStats(
30 `$type` = "com.jollywhoppers.minecraft.player.stats",
31 player = PlayerRef(
32 uuid = playerUuid.toString(),
33 username = "ExamplePlayer"
34 ),
35 statistics = listOf(
36 Statistic("minecraft:killed.minecraft.zombie", 42),
37 Statistic("minecraft:mined.minecraft.diamond_ore", 15)
38 ),
39 playtimeMinutes = 180,
40 level = 25,
41 gamemode = "survival",
42 syncedAt = java.time.Instant.now().toString()
43 )
44
45 val ref = recordManager.createTypedRecord(
46 playerUuid = playerUuid,
47 collection = "com.jollywhoppers.minecraft.player.stats",
48 record = stats,
49 validate = true
50 ).getOrThrow()
51
52 logger.info("Created stats record: ${ref.uri}")
53 ref.uri
54 }
55
56 /**
57 * Example: Create an achievement record
58 */
59 suspend fun createAchievement(
60 playerUuid: UUID,
61 achievementId: String,
62 achievementName: String
63 ): Result<String> = runCatching {
64 val achievement = Achievement(
65 `$type` = "com.jollywhoppers.minecraft.achievement",
66 player = PlayerRef(playerUuid.toString(), "ExamplePlayer"),
67 achievementId = achievementId,
68 achievementName = achievementName,
69 achievedAt = java.time.Instant.now().toString(),
70 category = "adventure"
71 )
72
73 val ref = recordManager.createTypedRecord(
74 playerUuid = playerUuid,
75 collection = "com.jollywhoppers.minecraft.achievement",
76 record = achievement
77 ).getOrThrow()
78
79 logger.info("Achievement unlocked: ${ref.uri}")
80 ref.uri
81 }
82
83 // ============================================================================
84 // EXAMPLE 2: Creating/Updating Singleton Records (Profiles)
85 // ============================================================================
86
87 /**
88 * Example: Create or update player profile (singleton with rkey "self")
89 * Uses putRecord for upsert behavior
90 */
91 suspend fun updatePlayerProfile(
92 playerUuid: UUID,
93 displayName: String?,
94 bio: String?
95 ): Result<String> = runCatching {
96 val profile = PlayerProfile(
97 `$type` = "com.jollywhoppers.minecraft.player.profile",
98 player = PlayerRef(playerUuid.toString(), "ExamplePlayer"),
99 displayName = displayName,
100 bio = bio,
101 publicStats = true,
102 publicSessions = true,
103 updatedAt = java.time.Instant.now().toString()
104 )
105
106 val ref = recordManager.putTypedRecord(
107 playerUuid = playerUuid,
108 collection = "com.jollywhoppers.minecraft.player.profile",
109 rkey = "self", // Singleton record uses literal "self"
110 record = profile
111 ).getOrThrow()
112
113 logger.info("Profile updated: ${ref.uri}")
114 ref.uri
115 }
116
117 // ============================================================================
118 // EXAMPLE 3: Reading Records
119 // ============================================================================
120
121 /**
122 * Example: Get a specific record by URI
123 */
124 suspend fun getPlayerProfile(playerUuid: UUID): Result<PlayerProfile> = runCatching {
125 val data = recordManager.getTypedRecord<PlayerProfile>(
126 playerUuid = playerUuid,
127 collection = "com.jollywhoppers.minecraft.player.profile",
128 rkey = "self"
129 ).getOrThrow()
130
131 logger.info("Retrieved profile for ${data.value.player.username}")
132 data.value
133 }
134
135 /**
136 * Example: Get a specific stats record by its TID
137 */
138 suspend fun getStatsRecord(playerUuid: UUID, tid: String): Result<PlayerStats> = runCatching {
139 val data = recordManager.getTypedRecord<PlayerStats>(
140 playerUuid = playerUuid,
141 collection = "com.jollywhoppers.minecraft.player.stats",
142 rkey = tid
143 ).getOrThrow()
144
145 logger.info("Retrieved stats: Level ${data.value.level}, ${data.value.playtimeMinutes}min playtime")
146 data.value
147 }
148
149 // ============================================================================
150 // EXAMPLE 4: Listing Records with Pagination
151 // ============================================================================
152
153 /**
154 * Example: List recent achievements (paginated)
155 */
156 suspend fun listRecentAchievements(
157 playerUuid: UUID,
158 limit: Int = 10
159 ): Result<List<Achievement>> = runCatching {
160 val result = recordManager.listRecords(
161 playerUuid = playerUuid,
162 collection = "com.jollywhoppers.minecraft.achievement",
163 limit = limit,
164 reverse = true // Most recent first
165 ).getOrThrow()
166
167 val achievements = result.records.mapNotNull { recordData ->
168 try {
169 kotlinx.serialization.json.Json.decodeFromJsonElement(serializer<Achievement>(), recordData.value)
170 } catch (e: Exception) {
171 logger.warn("Failed to parse achievement record", e)
172 null
173 }
174 }
175
176 logger.info("Found ${achievements.size} recent achievements")
177 achievements
178 }
179
180 /**
181 * Example: List all stats with automatic pagination handling
182 */
183 suspend fun getAllPlayerStats(playerUuid: UUID): Result<List<PlayerStats>> = runCatching {
184 val records = recordManager.listAllRecords(
185 playerUuid = playerUuid,
186 collection = "com.jollywhoppers.minecraft.player.stats",
187 batchSize = 50,
188 maxRecords = 200 // Optional limit to prevent too many requests
189 ).getOrThrow()
190
191 val stats = records.mapNotNull { recordData ->
192 try {
193 kotlinx.serialization.json.Json.decodeFromJsonElement(serializer<PlayerStats>(), recordData.value)
194 } catch (e: Exception) {
195 logger.warn("Failed to parse stats record", e)
196 null
197 }
198 }
199
200 logger.info("Retrieved ${stats.size} stats records")
201 stats
202 }
203
204 /**
205 * Example: Manual pagination with cursor
206 */
207 suspend fun paginateThroughAchievements(playerUuid: UUID): Result<Unit> = runCatching {
208 var cursor: String? = null
209 var page = 1
210
211 do {
212 val result = recordManager.listRecords(
213 playerUuid = playerUuid,
214 collection = "com.jollywhoppers.minecraft.achievement",
215 limit = 20,
216 cursor = cursor
217 ).getOrThrow()
218
219 logger.info("Page $page: ${result.records.size} records")
220
221 // Process this batch
222 result.records.forEach { recordData ->
223 // Do something with each record
224 logger.debug("Processing record: ${recordData.uri}")
225 }
226
227 cursor = result.cursor
228 page++
229 } while (cursor != null)
230
231 logger.info("Processed $page pages total")
232 }
233
234 // ============================================================================
235 // EXAMPLE 5: Updating Records
236 // ============================================================================
237
238 /**
239 * Example: Update existing profile with race condition protection
240 */
241 suspend fun safelyUpdateProfile(
242 playerUuid: UUID,
243 newDisplayName: String
244 ): Result<String> = runCatching {
245 // First, get the current record to get its CID
246 val current = recordManager.getTypedRecord<PlayerProfile>(
247 playerUuid = playerUuid,
248 collection = "com.jollywhoppers.minecraft.player.profile",
249 rkey = "self"
250 ).getOrThrow()
251
252 // Create updated profile
253 val updated = current.value.copy(
254 displayName = newDisplayName,
255 updatedAt = java.time.Instant.now().toString()
256 )
257
258 // Use swapRecord to ensure we're updating the version we read
259 val ref = recordManager.putTypedRecord(
260 playerUuid = playerUuid,
261 collection = "com.jollywhoppers.minecraft.player.profile",
262 rkey = "self",
263 record = updated,
264 swapRecord = current.cid // Prevents race conditions
265 ).getOrThrow()
266
267 logger.info("Profile safely updated: ${ref.uri}")
268 ref.uri
269 }
270
271 // ============================================================================
272 // EXAMPLE 6: Deleting Records
273 // ============================================================================
274
275 /**
276 * Example: Delete a specific achievement
277 */
278 suspend fun deleteAchievement(
279 playerUuid: UUID,
280 achievementTid: String
281 ): Result<Unit> = runCatching {
282 recordManager.deleteRecord(
283 playerUuid = playerUuid,
284 collection = "com.jollywhoppers.minecraft.achievement",
285 rkey = achievementTid
286 ).getOrThrow()
287
288 logger.info("Achievement deleted: $achievementTid")
289 }
290
291 /**
292 * Example: Safe delete with CID verification
293 */
294 suspend fun safelyDeleteRecord(
295 playerUuid: UUID,
296 collection: String,
297 rkey: String
298 ): Result<Unit> = runCatching {
299 // Get current record to verify it exists
300 val current = recordManager.getRecord(
301 playerUuid = playerUuid,
302 collection = collection,
303 rkey = rkey
304 ).getOrThrow()
305
306 // Delete with CID verification
307 recordManager.deleteRecord(
308 playerUuid = playerUuid,
309 collection = collection,
310 rkey = rkey,
311 swapRecord = current.cid // Ensures we delete the exact version we saw
312 ).getOrThrow()
313
314 logger.info("Record safely deleted: $rkey")
315 }
316
317 // ============================================================================
318 // EXAMPLE 7: Batch Operations (Atomic Transactions)
319 // ============================================================================
320
321 /**
322 * Example: Atomically create multiple achievements
323 * All succeed or all fail together
324 */
325 suspend fun createMultipleAchievements(
326 playerUuid: UUID,
327 achievements: List<Pair<String, String>> // (id, name) pairs
328 ): Result<Unit> = runCatching {
329 val writes = achievements.map { (id, name) ->
330 val achievement = Achievement(
331 `$type` = "com.jollywhoppers.minecraft.achievement",
332 player = PlayerRef(playerUuid.toString(), "ExamplePlayer"),
333 achievementId = id,
334 achievementName = name,
335 achievedAt = java.time.Instant.now().toString(),
336 category = "batch_unlock"
337 )
338
339 RecordManager.WriteOperation.Create(
340 collection = "com.jollywhoppers.minecraft.achievement",
341 value = kotlinx.serialization.json.Json.encodeToJsonElement(serializer<Achievement>(), achievement)
342 )
343 }
344
345 recordManager.applyWrites(
346 playerUuid = playerUuid,
347 writes = writes
348 ).getOrThrow()
349
350 logger.info("Created ${achievements.size} achievements atomically")
351 }
352
353 /**
354 * Example: Atomic batch update - create stats, update profile
355 */
356 suspend fun syncPlayerDataAtomically(
357 playerUuid: UUID,
358 stats: PlayerStats,
359 profileUpdate: PlayerProfile
360 ): Result<Unit> = runCatching {
361 val writes = listOf(
362 // Create new stats record
363 RecordManager.WriteOperation.Create(
364 collection = "com.jollywhoppers.minecraft.player.stats",
365 value = kotlinx.serialization.json.Json.encodeToJsonElement(serializer<PlayerStats>(), stats)
366 ),
367 // Update profile
368 RecordManager.WriteOperation.Update(
369 collection = "com.jollywhoppers.minecraft.player.profile",
370 rkey = "self",
371 value = kotlinx.serialization.json.Json.encodeToJsonElement(serializer<PlayerProfile>(), profileUpdate)
372 )
373 )
374
375 recordManager.applyWrites(
376 playerUuid = playerUuid,
377 writes = writes
378 ).getOrThrow()
379
380 logger.info("Player data synced atomically")
381 }
382
383 // ============================================================================
384 // EXAMPLE 8: Real-World Use Case - Stats Syncing
385 // ============================================================================
386
387 /**
388 * Complete example: Sync player statistics on logout
389 */
390 suspend fun onPlayerLogout(playerUuid: UUID, username: String): Result<Unit> = runCatching {
391 logger.info("Syncing stats for player logout: $username")
392
393 // Gather current statistics
394 val stats = PlayerStats(
395 `$type` = "com.jollywhoppers.minecraft.player.stats",
396 player = PlayerRef(playerUuid.toString(), username),
397 statistics = gatherMinecraftStats(playerUuid),
398 playtimeMinutes = calculatePlaytime(playerUuid),
399 level = getPlayerLevel(playerUuid),
400 gamemode = getCurrentGamemode(playerUuid),
401 syncedAt = java.time.Instant.now().toString()
402 )
403
404 // Create the record
405 val ref = recordManager.createTypedRecord(
406 playerUuid = playerUuid,
407 collection = "com.jollywhoppers.minecraft.player.stats",
408 record = stats
409 ).getOrThrow()
410
411 logger.info("Stats synced successfully: ${ref.uri}")
412 }
413
414 /**
415 * Complete example: Achievement unlocked workflow
416 */
417 suspend fun onAchievementUnlocked(
418 playerUuid: UUID,
419 achievementKey: String
420 ): Result<Unit> = runCatching {
421 // Check if already unlocked by listing existing achievements
422 val existing = recordManager.listAllRecords(
423 playerUuid = playerUuid,
424 collection = "com.jollywhoppers.minecraft.achievement"
425 ).getOrThrow()
426
427 val alreadyUnlocked = existing.any { recordData ->
428 try {
429 val achievement = kotlinx.serialization.json.Json.decodeFromJsonElement(serializer<Achievement>(), recordData.value)
430 achievement.achievementId == achievementKey
431 } catch (e: Exception) {
432 false
433 }
434 }
435
436 if (alreadyUnlocked) {
437 logger.info("Achievement $achievementKey already unlocked")
438 return@runCatching
439 }
440
441 // Create new achievement record
442 val achievement = Achievement(
443 `$type` = "com.jollywhoppers.minecraft.achievement",
444 player = PlayerRef(playerUuid.toString(), "Player"),
445 achievementId = achievementKey,
446 achievementName = getAchievementName(achievementKey),
447 achievedAt = java.time.Instant.now().toString(),
448 category = getAchievementCategory(achievementKey)
449 )
450
451 recordManager.createTypedRecord(
452 playerUuid = playerUuid,
453 collection = "com.jollywhoppers.minecraft.achievement",
454 record = achievement
455 ).getOrThrow()
456
457 logger.info("New achievement unlocked: $achievementKey")
458 }
459
460 // ============================================================================
461 // Helper Methods (Mock implementations)
462 // ============================================================================
463
464 private fun gatherMinecraftStats(uuid: UUID): List<Statistic> {
465 // In real implementation, query Minecraft's StatisticsManager
466 return listOf(
467 Statistic("minecraft:killed.minecraft.zombie", 42),
468 Statistic("minecraft:mined.minecraft.diamond_ore", 15)
469 )
470 }
471
472 private fun calculatePlaytime(uuid: UUID): Int {
473 // In real implementation, calculate from play time stat
474 return 180
475 }
476
477 private fun getPlayerLevel(uuid: UUID): Int {
478 // In real implementation, get from player.experienceLevel
479 return 25
480 }
481
482 private fun getCurrentGamemode(uuid: UUID): String {
483 // In real implementation, get from player.gameMode
484 return "survival"
485 }
486
487 private fun getAchievementName(key: String): String {
488 // In real implementation, look up from Minecraft's advancement system
489 return "Example Achievement"
490 }
491
492 private fun getAchievementCategory(key: String): String {
493 // In real implementation, categorize by advancement tree
494 return "adventure"
495 }
496
497 // ============================================================================
498 // Data Classes
499 // ============================================================================
500
501 @Serializable
502 data class PlayerRef(
503 val uuid: String,
504 val username: String
505 )
506
507 @Serializable
508 data class Statistic(
509 val key: String,
510 val value: Int
511 )
512
513 @Serializable
514 data class PlayerStats(
515 val `$type`: String,
516 val player: PlayerRef,
517 val statistics: List<Statistic>,
518 val playtimeMinutes: Int,
519 val level: Int,
520 val gamemode: String,
521 val syncedAt: String
522 )
523
524 @Serializable
525 data class PlayerProfile(
526 val `$type`: String,
527 val player: PlayerRef,
528 val displayName: String?,
529 val bio: String?,
530 val publicStats: Boolean,
531 val publicSessions: Boolean,
532 val updatedAt: String
533 )
534
535 @Serializable
536 data class Achievement(
537 val `$type`: String,
538 val player: PlayerRef,
539 val achievementId: String,
540 val achievementName: String,
541 val achievedAt: String,
542 val category: String
543 )
544}
545
546/**
547 * Quick Usage Examples:
548 *
549 * ```kotlin
550 * val examples = RecordManagerExamples(sessionManager)
551 *
552 * // Create a stats snapshot
553 * examples.createPlayerStats(playerUuid)
554 * .onSuccess { uri -> logger.info("Created: $uri") }
555 * .onFailure { error -> logger.error("Failed", error) }
556 *
557 * // Get player profile
558 * examples.getPlayerProfile(playerUuid)
559 * .onSuccess { profile -> displayProfile(profile) }
560 * .onFailure { error -> showError(error) }
561 *
562 * // List recent achievements
563 * examples.listRecentAchievements(playerUuid, limit = 5)
564 * .onSuccess { achievements ->
565 * achievements.forEach { logger.info(it.achievementName) }
566 * }
567 *
568 * // Update profile safely
569 * examples.safelyUpdateProfile(playerUuid, "New Display Name")
570 * .onSuccess { logger.info("Profile updated") }
571 *
572 * // Sync on logout
573 * examples.onPlayerLogout(playerUuid, player.name)
574 * ```
575 */