A Minecraft Fabric mod that connects the game with ATProtocol ⛏️
at main 575 lines 20 kB view raw
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 */